メインコンテンツへスキップ
見出し画像

有料ソフト不要。PDFツール6つを1つにまとめたら、爆速で作業が終わるようになった


    画像

    こんにちは、ゆうき建築AI活用です。

    一級建築士・一級建築施工管理技士として、設計から施工管理まで幅広く実務をこなしています。現在は地場の工務店で木造を中心に老人ホームやマンションの設計を担当し、ゼネコンで鉄骨造の現場代理人経験も複数あり。建築×AI活用を実践しながら、日々の業務効率化に取り組んでいます。


    結論:入り口を1つにしたら、誰でも使えるツールになった

    PDF作業でこんなことありませんか?

    容量が重すぎてメール添付できない
    スキャンした書類の向きがバラバラ
    図面の変更箇所を見比べたい
    構造図と意匠図を重ねて確認したい
    こういった作業、実は専用ソフトがなくても無料でできます。

    今回、自分用に作っていた6つのPDF処理ツールを、Gensparkで1つのWebアプリにまとめて公開しました。

    👉 PDF ツールボックス

    画像

    ブラウザだけで完結。インストール不要。建築図面はもちろん、営業資料・契約書・報告書など、あらゆる業務で使えます。

    しかも、オフラインツールなのでセキュリティも安心。
    アップロードしたファイルは外部サーバーに送信されず、すべてブラウザ内で処理されます。

    なぜ作ったのか
    もともと自分用に、6つの機能を持つツールをHTML形式で作っていました。

    でも問題がありました。

    機能ごとにファイルが分かれている
    どれを開けばいいか迷う
    他人に共有しづらい
    そこで、入り口を1つにまとめることにしました。

    結果、誰でも直感的に使えるツールになったんです。

    Gensparkでウェブ公開することで、URLひとつで誰でもアクセスできるようになりました。

    6つの機能と使いどころ
    ① PDF容量削減
    編集後のPDFや、画像が重いPDFを圧縮します。
    仕組みは「PDFをPDF印刷する」だけ。シンプルですが効果抜群。

    使う場面:
    プレゼン資料を作成後、容量が数十MBになってしまった時
    メール添付の容量制限に引っかかった時


    ② PDF自動回転
    縦A4・横A4が混ざってスキャンされたPDFを、自動で向きを調整してくれます。

    使う場面:複合機でまとめてスキャンしたら、向きがバラバラだった時
    手作業で1ページずつ回転するのが面倒な時


    ③ PDF変換
    PDFをjpg/pngに変換、逆も可能です。

    使う場面:図面をSNSやプレゼンで画像として使いたい時
    逆に、複数の画像をまとめてPDF化したい時

    ④ PDF比較
    2つのPDFを並べて、変更箇所を確認できます。

    使う場面:図面の変更前・変更後を見比べたい時
    契約書の修正箇所をチェックしたい時

    ⑤ PDF重ね図(オーバーレイ)
    複数のPDFを重ね合わせて表示します。

    使う場面:構造図を意匠図に重ねて整合確認したい時
    基礎伏図と土台伏図を重ねてチェックしたい時
    営業資料で複数データを重ねて比較したい時

    ⑥ PDF計測
    図面の一部寸法記載が無くても、簡易的に計測できたり、CADデータが貰えなくても面積を測定したりできます。
    ※CADではないので誤差があります。

    使う場面:寸法の記載がない図面で、おおよその長さを知りたい時。PDF上で面積測定したい時。


    セキュリティ面も安心:すべてブラウザ内で処理
    このツールの大きな特徴は、完全にオフラインで動作することです。

    アップロードしたファイルは外部サーバーに送信されません
    すべての処理がブラウザ内で完結します
    機密情報を含む図面や契約書も、安心して使えます
    オンラインで公開していますが、実際にはオフラインでも稼働するツールです。
    6つのツールのコードを記事末尾に掲載しているので、ローカル環境で試してもらえれば分かるはずです。

    4つのフリーソフトと組み合わせれば最強
    今回公開したツールに加えて、以下の4つのフリーソフトを組み合わせれば、有料ソフトは不要です。

    オブジェクトの一括移動以外は、大体できます。
    一括移動はPythonで作りましたので、またどこかで

    PDF-XChange Viewer(編集)
    文字や図形の書き込み、写真の貼り付けができます。
    無料なのに高機能。AdobeやEditorと違って、写真貼り付けも無料で可能。
    👉



    CUBE PDF(仮想プリンタ)
    「印刷」からPDFを作成できる仮想プリンタ。
    ExcelやWordなど、あらゆるファイルをPDF化できます。
    👉


    CUBE PDF Utility(並べ替え)
    PDFのページ順を入れ替えたり、回転したりできます。
    👉


    CUBE PDF Page(結合・分割)
    複数のPDFを1つに結合したり、逆に分割したりできます。
    👉


    これらを使い分けることで、PDF作業のほぼすべてを無料で完結できます。



    まとめ
    今回公開した「PDFツールボックス」は、自分の実務で使っていたものを、誰でも使いやすい形に整えたものです。

    バラバラだったツールの入り口を1つにまとめたことで、迷わず使えるようになりました。

    ブラウザだけで完結
    インストール不要
    完全無料
    オフライン動作でセキュリティも安心
    建築図面だけでなく、営業資料・契約書・報告書など、あらゆる業務で使えます。

    ぜひ一度試してみてください。

    👉 PDF ツールボックス


    もし役に立ったら、スキボタンを押していただけると嬉しいです。
    スレッズとXもフォローしていただけると励みになります!

    付録:6つのツールのコード
    オフラインで稼働することを試してもらえれば分かるはず。
    オンライン公開していますが、実際にはオフラインでも動作するツール群です。

    統合と公開のためにオンライン公開しましたが、本質はローカルで完結するツールです。

    以下、各ツールのコードを掲載します。

    ① PDF容量削減

    <!DOCTYPE html>
    <html lang="ja">
    <head>
    <style>
      .preloader {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: #fff;
        display: flex;
        justify-content: center;
        align-items: center;
        gap: 20px;
        z-index: 9999;
        animation: fadeOut 0.1s 0.3s forwards;
      }
      .glasses {
        display: flex;
        align-items: center;
        animation: swing 0.3s ease-in-out infinite alternate;
        transform-origin: top center;
      }
      .lens {
        width: 40px;
        height: 20px;
        border: 3px solid #888;
        border-radius: 4px;
        background: rgba(200,200,200,0.2);
      }
      .bridge {
        width: 12px;
        height: 3px;
        background: #888;
      }
      .temple {
        width: 20px;
        height: 3px;
        background: #888;
      }
      .pencil {
        font-size: 48px;
        animation: swing 0.3s ease-in-out infinite alternate;
        animation-delay: 0.15s;
        transform-origin: top center;
        display: inline-block;
      }
      @keyframes swing {
        0% { transform: rotate(-15deg); }
        100% { transform: rotate(15deg); }
      }
      @keyframes fadeOut {
        to { opacity: 0; visibility: hidden; }
      }
    </style>
    <div class="preloader">
      <div class="glasses">
        <div class="temple"></div>
        <div class="lens"></div>
        <div class="bridge"></div>
        <div class="lens"></div>
        <div class="temple"></div>
      </div>
      <span class="pencil">✏️</span>
    </div>
    
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>PDF容量最適化ツール(一括処理)</title>
        <script src="https://unpkg.com/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
        <style>
            * { box-sizing: border-box; }
            body {
                font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
                max-width: 900px;
                margin: 0 auto;
                padding: 20px;
                background: linear-gradient(135deg, #667eea 0%,  #764ba2 100%);
                min-height: 100vh;
            }
            .container {
                background: white;
                border-radius: 15px;
                padding: 30px;
                box-shadow: 0 20px 60px rgba(0,0,0,0.3);
            }
            h1 { color: #333; text-align: center; margin-bottom: 10px; }
            .subtitle { text-align: center; color: #666; margin-bottom: 30px; font-size: 14px; }
            .upload-area {
                border: 3px dashed #667eea;
                border-radius: 10px;
                padding: 50px 20px;
                text-align: center;
                cursor: pointer;
                transition: all 0.3s;
                background: #f8f9fa;
                margin-bottom: 20px;
            }
            .upload-area:hover { background: #e9ecef; border-color: #764ba2; }
            .upload-area.dragover { background: #e3f2fd; border-color: #2196f3; }
            .upload-area.has-file { background: #e8f5e9; border-color: #4caf50; }
            input[type="file"] { display: none; }
            .file-list {
                margin: 20px 0;
                max-height: 200px;
                overflow-y: auto;
                border: 1px solid #ddd;
                border-radius: 8px;
                display: none;
            }
            .file-item {
                display: flex;
                justify-content: space-between;
                align-items: center;
                padding: 10px 15px;
                border-bottom: 1px solid #eee;
                font-size: 14px;
            }
            .file-item:last-child { border-bottom: none; }
            .file-item .file-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
            .file-item .file-size { color: #666; margin-left: 10px; }
            .file-item .file-status { margin-left: 10px; font-weight: bold; }
            .file-item .status-waiting { color: #999; }
            .file-item .status-processing { color: #2196f3; }
            .file-item .status-done { color: #4caf50; }
            .file-item .status-error { color: #dc3545; }
            .file-item .remove-btn {
                background: #dc3545;
                color: white;
                border: none;
                border-radius: 4px;
                padding: 2px 8px;
                cursor: pointer;
                margin-left: 10px;
                font-size: 12px;
            }
            .file-item .remove-btn:hover { background: #c82333; }
            .settings {
                margin: 30px 0;
                padding: 20px;
                background: #f8f9fa;
                border-radius: 10px;
            }
            .settings-title {
                font-size: 18px;
                font-weight: bold;
                color: #333;
                margin-bottom: 20px;
                border-bottom: 2px solid #667eea;
                padding-bottom: 10px;
            }
            .setting-group { margin-bottom: 25px; }
            label { display: block; margin-bottom: 8px; color: #555; font-weight: bold; }
            select {
                width: 100%;
                padding: 12px;
                border: 2px solid #ddd;
                border-radius: 8px;
                font-size: 14px;
                transition: border-color 0.3s;
                background: white;
            }
            select:focus { outline: none; border-color: #667eea; }
            .page-size-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; margin-top: 10px; }
            .size-btn {
                padding: 15px 10px;
                border: 2px solid #ddd;
                background: white;
                border-radius: 8px;
                cursor: pointer;
                text-align: center;
                transition: all 0.3s;
            }
            .size-btn.active {
                border-color: #667eea;
                background: linear-gradient(135deg, #667eea15 0%, #764ba215 100%);
                transform: scale(1.05);
            }
            .size-btn:hover:not(.active) { background: #f8f9fa; border-color: #999; }
            .size-name { font-weight: bold; color: #333; margin-bottom: 5px; }
            .size-dims { font-size: 11px; color: #666; }
            .button-group { display: flex; gap: 15px; margin-bottom: 20px; }
            .button-group button { flex: 1; }
            button {
                background: linear-gradient(135deg, #667eea 0%,  #764ba2 100%);
                color: white;
                border: none;
                padding: 15px 30px;
                border-radius: 25px;
                cursor: pointer;
                font-size: 16px;
                font-weight: bold;
                transition: all 0.3s;
                box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4);
            }
            button:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(102, 126, 234, 0.5); }
            button:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
            .reset-btn {
                background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
                box-shadow: 0 4px 15px rgba(220, 53, 69, 0.4);
            }
            .reset-btn:hover:not(:disabled) { box-shadow: 0 6px 20px rgba(220, 53, 69, 0.5); }
            .progress { display: none; margin: 20px 0; }
            .progress-bar {
                width: 100%;
                height: 35px;
                background: #e9ecef;
                border-radius: 20px;
                overflow: hidden;
                box-shadow: inset 0 2px 4px rgba(0,0,0,0.1);
            }
            .progress-fill {
                height: 100%;
                background: linear-gradient(135deg, #667eea 0%,  #764ba2 100%);
                width: 0;
                transition: width 0.3s;
                display: flex;
                align-items: center;
                justify-content: center;
                color: white;
                font-weight: bold;
                font-size: 14px;
            }
            .result-info {
                display: none;
                margin-top: 20px;
                padding: 20px;
                background: #d4edda;
                border: 2px solid #28a745;
                border-radius: 10px;
                text-align: center;
            }
            .result-info .result-title { font-size: 20px; font-weight: bold; color: #155724; margin-bottom: 10px; }
            .result-stats { display: flex; justify-content: space-around; margin-top: 15px; }
            .result-stat { text-align: center; }
            .result-label { font-size: 12px; color: #155724; opacity: 0.8; }
            .result-value { font-size: 28px; font-weight: bold; color: #155724; }
            .warning {
                background: #fff3cd;
                border: 1px solid #ffc107;
                border-radius: 8px;
                padding: 15px;
                margin-top: 20px;
                font-size: 13px;
                color: #856404;
            }
            .warning-title { font-weight: bold; margin-bottom: 5px; display: flex; align-items: center; gap: 5px; }
            @keyframes fadeIn { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
            .fade-in { animation: fadeIn 0.3s ease-in-out; }
        </style>
    </head>
    <body>
        <div class="container">
            <h1>🖨️ PDF容量最適化ツール</h1>
            <p class="subtitle">複数PDFファイルを一括で圧縮します(順番に処理)</p>
            
            <div class="upload-area" id="uploadArea">
                <p style="margin: 0; font-size: 18px;">
                    📁 PDFファイルをドラッグ&ドロップ<br>
                    <span style="font-size: 14px; color: #666;">または</span><br>
                    <strong>クリックして選択(複数可)</strong>
                </p>
                <input type="file" id="fileInput" accept=".pdf" multiple>
            </div>
    
            <div class="file-list" id="fileList"></div>
    
            <div class="button-group">
                <button id="convertBtn" disabled>🖨️ 一括最適化を実行</button>
                <button id="resetBtn" class="reset-btn" onclick="resetAll()">🔄 リセット</button>
            </div>
    
            <div class="settings">
                <div class="settings-title">⚙️ 設定</div>
                <div class="setting-group">
                    <label>📐 用紙サイズ</label>
                    <div class="page-size-grid">
                        <div class="size-btn active" data-size="A4" onclick="selectPageSize('A4')">
                            <div class="size-name">A4</div>
                            <div class="size-dims">210×297mm</div>
                        </div>
                        <div class="size-btn" data-size="A3" onclick="selectPageSize('A3')">
                            <div class="size-name">A3</div>
                            <div class="size-dims">297×420mm</div>
                        </div>
                        <div class="size-btn" data-size="B4" onclick="selectPageSize('B4')">
                            <div class="size-name">B4</div>
                            <div class="size-dims">257×364mm</div>
                        </div>
                        <div class="size-btn" data-size="B5" onclick="selectPageSize('B5')">
                            <div class="size-name">B5</div>
                            <div class="size-dims">182×257mm</div>
                        </div>
                        <div class="size-btn" data-size="Letter" onclick="selectPageSize('Letter')">
                            <div class="size-name">Letter</div>
                            <div class="size-dims">216×279mm</div>
                        </div>
                        <div class="size-btn" data-size="Original" onclick="selectPageSize('Original')">
                            <div class="size-name">元のサイズ</div>
                            <div class="size-dims">変更なし</div>
                        </div>
                    </div>
                </div>
                <div class="setting-group">
                    <label for="quality">🎨 出力品質</label>
                    <select id="quality">
                        <option value="lowest">最低品質(最小サイズ優先)</option>
                        <option value="low">低品質(小サイズ)</option>
                        <option value="low-medium">やや低品質(バランス型)</option>
                        <option value="medium" selected>標準品質(推奨)</option>
                        <option value="high">高品質(大サイズ)</option>
                        <option value="best">最高品質(印刷用)</option>
                    </select>
                </div>
                <div class="setting-group">
                    <label for="orientation">📄 用紙の向き</label>
                    <select id="orientation">
                        <option value="auto" selected>自動(コンテンツに合わせる)</option>
                        <option value="portrait">縦向き</option>
                        <option value="landscape">横向き</option>
                    </select>
                </div>
                <div class="setting-group">
                    <label for="margins">📏 余白</label>
                    <select id="margins">
                        <option value="none" selected>なし(フチなし)</option>
                        <option value="narrow">狭い(5mm)</option>
                        <option value="normal">標準(10mm)</option>
                        <option value="wide">広い(20mm)</option>
                    </select>
                </div>
            </div>
    
            <div class="progress" id="progress">
                <div class="progress-bar">
                    <div class="progress-fill" id="progressFill">処理中...</div>
                </div>
            </div>
    
            <div class="result-info" id="resultInfo">
                <div class="result-title">✅ 一括最適化完了!</div>
                <div class="result-stats">
                    <div class="result-stat">
                        <div class="result-label">処理ファイル数</div>
                        <div class="result-value" id="processedCount">-</div>
                    </div>
                    <div class="result-stat">
                        <div class="result-label">合計削減サイズ</div>
                        <div class="result-value" id="totalReduction">-</div>
                    </div>
                </div>
            </div>
    
            <div class="warning">
                <div class="warning-title">💡 品質設定の目安</div>
                • <strong>最低品質</strong>: ファイルサイズ最優先(画質50%、解像度1.5倍)<br>
                • <strong>低品質</strong>: 小さいサイズ(画質60%、解像度2.0倍)<br>
                • <strong>やや低品質</strong>: バランス重視(画質68%、解像度2.5倍)<br>
                • <strong>標準品質</strong>: 推奨設定(画質75%、解像度3.0倍)<br>
                • <strong>高品質</strong>: 品質重視(画質85%、解像度4.0倍)<br>
                • <strong>最高品質</strong>: 印刷用(画質95%、解像度5.0倍)
            </div>
        </div>
    
        <script>
            pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
            const CMAP_URL = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/cmaps/';
            const CMAP_PACKED = true;
            const STANDARD_FONT_DATA_URL = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.11.174/standard_fonts/';
            
            let pdfFiles = [];
            let selectedPageSize = 'A4';
            
            const uploadArea = document.getElementById('uploadArea');
            const fileInput = document.getElementById('fileInput');
            const fileList = document.getElementById('fileList');
            const convertBtn = document.getElementById('convertBtn');
            const resetBtn = document.getElementById('resetBtn');
            const progress = document.getElementById('progress');
            const progressFill = document.getElementById('progressFill');
            const resultInfo = document.getElementById('resultInfo');
            
            const PAGE_SIZES = {
                A4: { width: 595, height: 842 },
                A3: { width: 842, height: 1191 },
                B4: { width: 729, height: 1032 },
                B5: { width: 516, height: 729 },
                Letter: { width: 612, height: 792 },
                Original: null
            };
            
            function selectPageSize(size) {
                selectedPageSize = size;
                document.querySelectorAll('.size-btn').forEach(btn => {
                    btn.classList.toggle('active', btn.dataset.size === size);
                });
            }
            
            function resetAll() {
                pdfFiles = [];
                fileInput.value = '';
                uploadArea.classList.remove('has-file');
                fileList.style.display = 'none';
                fileList.innerHTML = '';
                resultInfo.style.display = 'none';
                progress.style.display = 'none';
                convertBtn.disabled = true;
                selectPageSize('A4');
                document.getElementById('quality').value = 'medium';
                document.getElementById('orientation').value = 'auto';
                document.getElementById('margins').value = 'none';
            }
            
            function renderFileList() {
                fileList.innerHTML = '';
                if (pdfFiles.length === 0) {
                    fileList.style.display = 'none';
                    uploadArea.classList.remove('has-file');
                    convertBtn.disabled = true;
                    return;
                }
                fileList.style.display = 'block';
                uploadArea.classList.add('has-file');
                convertBtn.disabled = false;
                
                pdfFiles.forEach((f, idx) => {
                    const item = document.createElement('div');
                    item.className = 'file-item';
                    item.id = `file-item-${idx}`;
                    item.innerHTML = `
                        <span class="file-name">${f.file.name}</span>
                        <span class="file-size">${formatFileSize(f.file.size)}</span>
                        <span class="file-status status-waiting" id="status-${idx}">待機中</span>
                        <button class="remove-btn" onclick="removeFile(${idx})">✕</button>
                    `;
                    fileList.appendChild(item);
                });
            }
            
            function removeFile(idx) {
                pdfFiles.splice(idx, 1);
                renderFileList();
            }
            
            function formatFileSize(bytes) {
                if (bytes < 1024) return bytes + ' B';
                if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
                return (bytes / (1024 * 1024)).toFixed(2) + ' MB';
            }
            
            function getQualitySettings(quality) {
                switch(quality) {
                    case 'lowest': return { scale: 1.5, imageQuality: 0.5, compression: true };
                    case 'low': return { scale: 2.0, imageQuality: 0.6, compression: true };
                    case 'low-medium': return { scale: 2.5, imageQuality: 0.68, compression: true };
                    case 'medium': return { scale: 3.0, imageQuality: 0.75, compression: true };
                    case 'high': return { scale: 4.0, imageQuality: 0.85, compression: true };
                    case 'best': return { scale: 5.0, imageQuality: 0.95, compression: false };
                    default: return { scale: 3.0, imageQuality: 0.75, compression: true };
                }
            }
            
            uploadArea.addEventListener('click', () => fileInput.click());
            uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.classList.add('dragover'); });
            uploadArea.addEventListener('dragleave', () => uploadArea.classList.remove('dragover'));
            uploadArea.addEventListener('drop', (e) => {
                e.preventDefault();
                uploadArea.classList.remove('dragover');
                const files = Array.from(e.dataTransfer.files).filter(f => f.type === 'application/pdf');
                addFiles(files);
            });
            fileInput.addEventListener('change', (e) => {
                addFiles(Array.from(e.target.files));
                fileInput.value = '';
            });
            
            function addFiles(files) {
                files.forEach(file => {
                    pdfFiles.push({ file, status: 'waiting' });
                });
                renderFileList();
            }
            
            convertBtn.addEventListener('click', async () => {
                if (pdfFiles.length === 0) return;
                
                convertBtn.disabled = true;
                resetBtn.disabled = true;
                progress.style.display = 'block';
                resultInfo.style.display = 'none';
                
                document.querySelectorAll('.remove-btn').forEach(btn => btn.disabled = true);
                
                const quality = document.getElementById('quality').value;
                const orientation = document.getElementById('orientation').value;
                const margins = document.getElementById('margins').value;
                const qualitySettings = getQualitySettings(quality);
                
                let margin = 0;
                switch(margins) {
                    case 'narrow': margin = 14; break;
                    case 'normal': margin = 28; break;
                    case 'wide': margin = 57; break;
                }
                
                let totalBefore = 0, totalAfter = 0, processedCount = 0;
                
                for (let fileIdx = 0; fileIdx < pdfFiles.length; fileIdx++) {
                    const statusEl = document.getElementById(`status-${fileIdx}`);
                    statusEl.textContent = '処理中...';
                    statusEl.className = 'file-status status-processing';
                    
                    progressFill.style.width = ((fileIdx) / pdfFiles.length * 100) + '%';
                    progressFill.textContent = `ファイル ${fileIdx + 1}/${pdfFiles.length} 処理中...`;
                    
                    try {
                        const pdfFile = pdfFiles[fileIdx].file;
                        const arrayBuffer = await pdfFile.arrayBuffer();
                        
                        const loadingTask = pdfjsLib.getDocument({
                            data: arrayBuffer,
                            cMapUrl: CMAP_URL,
                            cMapPacked: CMAP_PACKED,
                            standardFontDataUrl: STANDARD_FONT_DATA_URL,
                            disableFontFace: false,
                            useSystemFonts: true,
                            verbosity: 0
                        });
                        
                        const srcPdf = await loadingTask.promise;
                        const numPages = srcPdf.numPages;
                        const pdfDoc = await PDFLib.PDFDocument.create();
                        
                        for (let i = 1; i <= numPages; i++) {
                            const page = await srcPdf.getPage(i);
                            const viewport = page.getViewport({ scale: qualitySettings.scale });
                            
                            const offscreenCanvas = new OffscreenCanvas(viewport.width, viewport.height);
                            const context = offscreenCanvas.getContext('2d', { alpha: false, desynchronized: true });
                            
                            context.fillStyle = '#ffffff';
                            context.fillRect(0, 0, viewport.width, viewport.height);
                            
                            const renderContext = {
                                canvasContext: context,
                                viewport: viewport,
                                intent: 'print',
                                annotationMode: 2,
                                renderTextLayer: false,
                                renderInteractiveForms: true
                            };
                            
                            await page.render(renderContext).promise;
                            
                            const blob = await offscreenCanvas.convertToBlob({
                                type: 'image/jpeg',
                                quality: qualitySettings.imageQuality
                            });
                            
                            let pageWidth, pageHeight;
                            if (selectedPageSize === 'Original') {
                                const originalViewport = page.getViewport({ scale: 1 });
                                pageWidth = originalViewport.width;
                                pageHeight = originalViewport.height;
                            } else {
                                const size = PAGE_SIZES[selectedPageSize];
                                if (orientation === 'auto') {
                                    const isContentLandscape = viewport.width > viewport.height;
                                    if (isContentLandscape) {
                                        pageWidth = Math.max(size.width, size.height);
                                        pageHeight = Math.min(size.width, size.height);
                                    } else {
                                        pageWidth = Math.min(size.width, size.height);
                                        pageHeight = Math.max(size.width, size.height);
                                    }
                                } else if (orientation === 'landscape') {
                                    pageWidth = Math.max(size.width, size.height);
                                    pageHeight = Math.min(size.width, size.height);
                                } else {
                                    pageWidth = Math.min(size.width, size.height);
                                    pageHeight = Math.max(size.width, size.height);
                                }
                            }
                            
                            const newPage = pdfDoc.addPage([pageWidth, pageHeight]);
                            const imageBytes = new Uint8Array(await blob.arrayBuffer());
                            const image = await pdfDoc.embedJpg(imageBytes);
                            
                            const drawWidth = pageWidth - (margin * 2);
                            const drawHeight = pageHeight - (margin * 2);
                            const imageAspect = viewport.width / viewport.height;
                            const pageAspect = drawWidth / drawHeight;
                            
                            let finalWidth, finalHeight, x, y;
                            if (imageAspect > pageAspect) {
                                finalWidth = drawWidth;
                                finalHeight = drawWidth / imageAspect;
                                x = margin;
                                y = margin + (drawHeight - finalHeight) / 2;
                            } else {
                                finalHeight = drawHeight;
                                finalWidth = drawHeight * imageAspect;
                                x = margin + (drawWidth - finalWidth) / 2;
                                y = margin;
                            }
                            
                            newPage.drawImage(image, { x, y, width: finalWidth, height: finalHeight });
                        }
                        
                        const pdfBytes = await pdfDoc.save({
                            useObjectStreams: qualitySettings.compression,
                            addDefaultPage: false,
                            objectsPerTick: 200,
                        });
                        
                        const blob = new Blob([pdfBytes], { type: 'application/pdf' });
                        const url = URL.createObjectURL(blob);
                        const a = document.createElement('a');
                        a.href = url;
                        const timestamp = new Date().toISOString().slice(0, 19).replace(/[:-]/g, '').replace('T', '_');
                        a.download = pdfFile.name.replace('.pdf', `_optimized_${timestamp}.pdf`);
                        a.click();
                        URL.revokeObjectURL(url);
                        
                        totalBefore += pdfFile.size;
                        totalAfter += pdfBytes.length;
                        processedCount++;
                        
                        statusEl.textContent = '完了';
                        statusEl.className = 'file-status status-done';
                        
                    } catch (error) {
                        console.error('Error:', error);
                        statusEl.textContent = 'エラー';
                        statusEl.className = 'file-status status-error';
                    }
                }
                
                progressFill.style.width = '100%';
                progressFill.textContent = '全て完了!';
                
                document.getElementById('processedCount').textContent = `${processedCount}件`;
                const reduction = totalBefore - totalAfter;
                document.getElementById('totalReduction').textContent = reduction > 0 ? formatFileSize(reduction) : '0 B';
                
                resultInfo.style.display = 'block';
                resultInfo.classList.add('fade-in');
                
                setTimeout(() => {
                    progress.style.display = 'none';
                    convertBtn.disabled = false;
                    resetBtn.disabled = false;
                }, 2000);
            });
        </script>
    </body>
    </html>
    

    ② PDF自動回転

    <!DOCTYPE html>
    <html lang="ja">
    <head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PDF 自動回転ツール</title>
    <style>
    .preloader {
      position: fixed; top: 0; left: 0; width: 100%; height: 100%;
      background: #1a1a2e; display: flex; justify-content: center; align-items: center; gap: 20px;
      z-index: 9999; animation: fadeOut 0.5s 1.5s forwards;
    }
    .glasses {
      display: flex; align-items: center;
      animation: swing 0.3s ease-in-out infinite alternate; transform-origin: top center;
    }
    .lens { width: 40px; height: 20px; border: 3px solid #00d2ff; border-radius: 4px; background: rgba(0,210,255,0.1); }
    .bridge { width: 12px; height: 3px; background: #00d2ff; }
    .temple { width: 20px; height: 3px; background: #00d2ff; }
    .pencil {
      font-size: 48px; animation: swing 0.3s ease-in-out infinite alternate;
      animation-delay: 0.15s; transform-origin: top center; display: inline-block;
    }
    @keyframes swing { 0% { transform: rotate(-15deg); } 100% { transform: rotate(15deg); } }
    @keyframes fadeOut { to { opacity: 0; visibility: hidden; pointer-events: none; } }
    
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: 'Segoe UI', 'Meiryo', sans-serif; background: #1a1a2e; color: #eee; min-height: 100vh; }
    .container { max-width: 1400px; margin: 0 auto; padding: 20px; }
    h1 { text-align: center; margin: 20px 0; font-size: 1.5em; color: #00d2ff; }
    
    .drop-zone {
      border: 3px dashed #00d2ff; border-radius: 16px; padding: 50px 20px;
      text-align: center; cursor: pointer; transition: all 0.3s;
      background: rgba(0,210,255,0.05); margin-bottom: 20px; position: relative;
    }
    .drop-zone.dragover { background: rgba(0,210,255,0.18); border-color: #00ff88; }
    .drop-zone:hover { background: rgba(0,210,255,0.10); }
    .drop-zone p { font-size: 1.1em; color: #aaa; pointer-events: none; }
    .drop-zone .file-name { font-size: 0.95em; color: #00ff88; margin-top: 12px; min-height: 1.2em; pointer-events: none; }
    
    .mode-select { display: none; text-align: center; margin-bottom: 20px; }
    .mode-select .mode-btn {
      padding: 16px 40px; border: 2px solid #00d2ff; border-radius: 12px; cursor: pointer;
      font-size: 1.1em; font-weight: bold; background: #16213e; color: #00d2ff;
      margin: 0 10px; transition: all 0.2s;
    }
    .mode-select .mode-btn:hover { background: #00d2ff; color: #1a1a2e; }
    
    .controls { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }
    .btn {
      padding: 10px 24px; border: none; border-radius: 8px; cursor: pointer;
      font-size: 0.95em; font-weight: bold; transition: all 0.2s;
    }
    .btn-primary { background: #00d2ff; color: #1a1a2e; }
    .btn-primary:hover { background: #00ff88; }
    .btn-primary:disabled { background: #555; color: #888; cursor: not-allowed; }
    .btn-download { background: #00ff88; color: #1a1a2e; }
    .btn-download:hover { background: #00d2ff; }
    .btn-reset { background: #ff4466; color: #fff; }
    .btn-reset:hover { background: #ff6688; }
    
    .settings { display: none; gap: 16px; align-items: center; flex-wrap: wrap; margin-bottom: 20px; }
    .settings label { font-size: 0.85em; color: #aaa; }
    .settings select { background: #16213e; color: #eee; border: 1px solid #444; padding: 6px 10px; border-radius: 6px; }
    .settings input[type=checkbox] { margin-right: 4px; }
    
    .progress-area { margin-bottom: 20px; display: none; }
    .progress-bar-bg { background: #333; border-radius: 8px; height: 30px; overflow: hidden; position: relative; }
    .progress-bar-fill { background: linear-gradient(90deg, #00d2ff, #00ff88); height: 100%; width: 0%; transition: width 0.3s; }
    .progress-text { position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%); font-weight: bold; font-size: 0.85em; color: #fff; text-shadow: 0 0 4px rgba(0,0,0,0.8); }
    #statusText { margin-top: 8px; font-size: 0.9em; color: #aaa; }
    
    .log-area {
      background: #0d0d1a; border: 1px solid #333; border-radius: 8px;
      padding: 12px; max-height: 200px; overflow-y: auto; font-family: monospace;
      font-size: 0.8em; margin-bottom: 20px; display: none;
    }
    .log-area .log-ok { color: #00ff88; }
    .log-area .log-rotate { color: #ffaa00; }
    .log-area .log-err { color: #ff4444; }
    
    /* === レビューエリア === */
    .review-area { display: none; margin-top: 20px; }
    .review-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 16px; flex-wrap: wrap; gap: 10px; }
    .review-header h2 { color: #00d2ff; font-size: 1.2em; }
    
    .review-main { display: flex; gap: 20px; align-items: flex-start; flex-wrap: wrap; }
    .preview-box {
      flex: 1; min-width: 300px; background: #0d0d1a; border-radius: 12px; padding: 16px;
      display: flex; flex-direction: column; align-items: center;
    }
    .preview-box canvas { max-width: 100%; max-height: 70vh; border-radius: 4px; background: #fff; }
    .preview-label { margin-top: 8px; font-size: 0.85em; color: #aaa; }
    
    /* ナビ+手動回転: プレビュー下 */
    .below-preview {
      display: flex; flex-direction: column; align-items: center; gap: 12px; margin-top: 16px; width: 100%;
    }
    .review-nav { display: flex; align-items: center; gap: 10px; }
    .review-nav .btn-nav {
      padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer;
      font-size: 1em; font-weight: bold; background: #16213e; color: #00d2ff; border: 1px solid #00d2ff;
    }
    .review-nav .btn-nav:hover { background: #00d2ff; color: #1a1a2e; }
    .review-nav .btn-nav:disabled { opacity: 0.3; cursor: not-allowed; }
    .review-nav .page-indicator { font-size: 1em; color: #aaa; min-width: 100px; text-align: center; }
    
    .manual-controls { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; justify-content: center; }
    .manual-controls .btn-rotate {
      padding: 10px 18px; border: none; border-radius: 8px; cursor: pointer;
      font-size: 0.9em; font-weight: bold; transition: all 0.2s;
    }
    .btn-rot-cw { background: #ffaa00; color: #1a1a2e; }
    .btn-rot-ccw { background: #ff8800; color: #1a1a2e; }
    .btn-rot-180 { background: #ff6644; color: #fff; }
    .btn-rot-reset { background: #555; color: #eee; }
    .rotation-info {
      background: #16213e; border-radius: 8px; padding: 8px 14px; text-align: center;
      font-size: 0.85em; color: #aaa;
    }
    .rotation-info .rot-value { font-size: 1.2em; color: #00ff88; font-weight: bold; }
    
    /* サムネイル */
    .thumb-strip {
      display: flex; gap: 8px; overflow-x: auto; padding: 10px 0; margin-top: 16px;
    }
    .thumb-strip .ts-item {
      flex-shrink: 0; width: 80px; cursor: pointer; border-radius: 6px;
      border: 2px solid transparent; padding: 4px; background: #16213e; text-align: center;
      transition: border-color 0.2s;
    }
    .thumb-strip .ts-item:hover { border-color: #00d2ff; }
    .thumb-strip .ts-item.active { border-color: #00ff88; }
    .thumb-strip .ts-item.modified { border-color: #ffaa00; }
    .thumb-strip .ts-item.active.modified { border-color: #00ff88; box-shadow: 0 0 0 2px #ffaa00; }
    .thumb-strip .ts-item img { width: 100%; border-radius: 3px; background: #fff; }
    .thumb-strip .ts-item .ts-label { font-size: 0.65em; color: #aaa; margin-top: 2px; }
    .thumb-strip .ts-item .ts-badge {
      font-size: 0.6em; font-weight: bold; display: inline-block;
      padding: 1px 4px; border-radius: 3px; margin-top: 2px;
    }
    
    /* 手動モードグリッド */
    .manual-grid {
      display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-top: 20px;
    }
    .mg-card {
      background: #16213e; border-radius: 10px; padding: 10px; text-align: center;
      display: flex; flex-direction: column; align-items: center;
      height: 380px;
    }
    .mg-card img { max-width: 100%; flex: 1; object-fit: contain; border-radius: 4px; background: #fff; min-height: 0; }
    .mg-card .mg-label { margin-top: 6px; flex-shrink: 0; }
    .mg-card .mg-info { flex-shrink: 0; }
    .mg-card button { flex-shrink: 0; }
    .mg-card img { max-width: 100%; border-radius: 4px; background: #fff; }
    .mg-card .mg-label { font-size: 0.8em; color: #00d2ff; margin: 6px 0 4px; font-weight: bold; }
    .mg-card .mg-btns { display: flex; gap: 4px; justify-content: center; flex-wrap: wrap; }
    .mg-card .mg-btns button {
      padding: 5px 10px; border: none; border-radius: 5px; cursor: pointer;
      font-size: 0.75em; font-weight: bold; background: #2a2a4a; color: #eee;
    }
    .mg-card .mg-btns button:hover { background: #00d2ff; color: #1a1a2e; }
    .mg-card .mg-info { font-size: 0.7em; color: #aaa; margin-top: 4px; }
    </style>
    </head>
    <body>
    
    <div class="preloader">
      <div class="glasses">
        <div class="temple"></div><div class="lens"></div><div class="bridge"></div><div class="lens"></div><div class="temple"></div>
      </div>
      <span class="pencil">&#9999;&#65039;</span>
    </div>
    
    <div class="container">
      <h1>PDF 自動回転ツール(OCR文字方向検出)</h1>
    
      <div class="drop-zone" id="dropZone">
        <p>PDFをドラッグ&ドロップ or クリックして選択</p>
        <div class="file-name" id="fileName"></div>
        <input type="file" id="fileInput" accept=".pdf" style="position:absolute;top:0;left:0;width:100%;height:100%;opacity:0;cursor:pointer;">
      </div>
    
      <!-- モード選択 -->
      <div class="mode-select" id="modeSelect">
        <button class="mode-btn" id="btnModeAuto">自動回転(OCR)</button>
        <button class="mode-btn" id="btnModeManual">手動回転</button>
      </div>
    
      <!-- 自動モード設定 -->
      <div class="settings" id="autoSettings">
        <label>DPI: <select id="dpiSelect">
          <option value="150">150(高速)</option>
          <option value="200" selected>200(推奨)</option>
          <option value="300">300(高精度)</option>
        </select></label>
        <label>並列Worker数: <select id="workerCount">
          <option value="2">2</option>
          <option value="4" selected>4</option>
          <option value="6">6</option>
        </select></label>
        <label><input type="checkbox" id="chkAutoDL" checked> 自動ダウンロード</label>
      </div>
    
      <div class="controls" id="autoControls" style="display:none;">
        <button class="btn btn-primary" id="btnProcess">回転処理を開始</button>
        <button class="btn btn-download" id="btnDownload" style="display:none">回転済みPDFをダウンロード</button>
        <button class="btn btn-reset" id="btnReset" style="display:none">リセット</button>
        <span id="timeInfo" style="font-size:0.85em; color:#aaa;"></span>
      </div>
    
      <div class="progress-area" id="progressArea">
        <div class="progress-bar-bg">
          <div class="progress-bar-fill" id="progressFill"></div>
          <div class="progress-text" id="progressText">0%</div>
        </div>
        <div id="statusText"></div>
      </div>
    
      <div class="log-area" id="logArea"></div>
    
      <!-- 自動後レビュー -->
      <div class="review-area" id="reviewArea">
        <div class="review-header">
          <h2>ページ確認・手動微調整</h2>
        </div>
        <div class="review-main">
          <div class="preview-box">
            <canvas id="previewCanvas"></canvas>
            <div class="preview-label" id="previewLabel"></div>
            <div class="below-preview">
              <div class="manual-controls">
                <div class="rotation-info">
                  <span class="rot-value" id="rotValue">0°</span>
                  <div id="rotSource" style="font-size:0.7em;margin-top:2px;"></div>
                </div>
                <button class="btn-rotate btn-rot-ccw" id="btnRotCCW">← 90°</button>
                <button class="btn-rotate btn-rot-cw" id="btnRotCW">90° →</button>
                <button class="btn-rotate btn-rot-180" id="btnRot180">180°</button>
                <button class="btn-rotate btn-rot-reset" id="btnRotReset">0°</button>
              </div>
              <div class="review-nav">
                <button class="btn-nav" id="btnPrev" disabled>◀ 前</button>
                <span class="page-indicator" id="pageIndicator">1 / 1</span>
                <button class="btn-nav" id="btnNext">次 ▶</button>
              </div>
            </div>
          </div>
        </div>
        <div class="thumb-strip" id="thumbStrip"></div>
      </div>
    
      <!-- 手動モード -->
      <div id="manualArea" style="display:none;">
        <div class="controls">
          <button class="btn btn-download" id="btnManualDL">回転済みPDFをダウンロード</button>
          <button class="btn btn-reset" id="btnManualReset">リセット</button>
        </div>
        <div id="manualStatus" style="font-size:0.9em;color:#aaa;margin-bottom:10px;"></div>
        <div class="manual-grid" id="manualGrid"></div>
      </div>
    </div>
    
    <script src="https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
    <script>
    pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
    
    var dropZone = document.getElementById('dropZone');
    var fileInput = document.getElementById('fileInput');
    var fileNameDiv = document.getElementById('fileName');
    var modeSelect = document.getElementById('modeSelect');
    var autoSettings = document.getElementById('autoSettings');
    var autoControls = document.getElementById('autoControls');
    var btnProcess = document.getElementById('btnProcess');
    var btnDownload = document.getElementById('btnDownload');
    var btnReset = document.getElementById('btnReset');
    var progressArea = document.getElementById('progressArea');
    var progressFill = document.getElementById('progressFill');
    var progressText = document.getElementById('progressText');
    var statusText = document.getElementById('statusText');
    var logArea = document.getElementById('logArea');
    var timeInfo = document.getElementById('timeInfo');
    var reviewArea = document.getElementById('reviewArea');
    var thumbStrip = document.getElementById('thumbStrip');
    var previewCanvas = document.getElementById('previewCanvas');
    var previewLabel = document.getElementById('previewLabel');
    var pageIndicator = document.getElementById('pageIndicator');
    var rotValue = document.getElementById('rotValue');
    var rotSource = document.getElementById('rotSource');
    var manualArea = document.getElementById('manualArea');
    var manualGrid = document.getElementById('manualGrid');
    var manualStatus = document.getElementById('manualStatus');
    
    var pdfArrayBuffer = null;
    var originalFileName = '';
    var pageData = [];
    var currentPage = 0;
    var currentMode = ''; // 'auto' or 'manual'
    
    /* === D&D === */
    dropZone.addEventListener('dragover', function(e) { e.preventDefault(); e.stopPropagation(); dropZone.classList.add('dragover'); });
    dropZone.addEventListener('dragleave', function(e) { e.preventDefault(); e.stopPropagation(); dropZone.classList.remove('dragover'); });
    dropZone.addEventListener('drop', function(e) {
      e.preventDefault(); e.stopPropagation(); dropZone.classList.remove('dragover');
      var files = e.dataTransfer && e.dataTransfer.files;
      if (files && files.length > 0) { fileInput.value = ''; handleFile(files[0]); }
    });
    fileInput.addEventListener('change', function(e) {
      if (e.target.files && e.target.files[0]) handleFile(e.target.files[0]);
    });
    
    function handleFile(file) {
      if (!file.name.toLowerCase().endsWith('.pdf')) { alert('PDFファイルを選択してください'); return; }
      originalFileName = file.name.replace(/\.pdf$/i, '');
      var reader = new FileReader();
      reader.onload = function(e) {
        pdfArrayBuffer = e.target.result;
        fileNameDiv.textContent = file.name + '(' + (file.size / 1024 / 1024).toFixed(1) + 'MB)';
        resetUI();
        modeSelect.style.display = 'block';
      };
      reader.readAsArrayBuffer(file);
    }
    
    function resetUI() {
      autoSettings.style.display = 'none';
      autoControls.style.display = 'none';
      btnDownload.style.display = 'none';
      btnReset.style.display = 'none';
      progressArea.style.display = 'none';
      progressFill.style.width = '0%';
      progressText.textContent = '0%';
      statusText.textContent = '';
      logArea.style.display = 'none';
      logArea.innerHTML = '';
      timeInfo.textContent = '';
      reviewArea.style.display = 'none';
      thumbStrip.innerHTML = '';
      manualArea.style.display = 'none';
      manualGrid.innerHTML = '';
      pageData = [];
      currentPage = 0;
      currentMode = '';
    }
    
    function fullReset() {
      pdfArrayBuffer = null; originalFileName = '';
      fileInput.value = ''; fileNameDiv.textContent = '';
      modeSelect.style.display = 'none';
      resetUI();
    }
    
    /* === モード選択 === */
    document.getElementById('btnModeAuto').addEventListener('click', function() {
      currentMode = 'auto';
      modeSelect.style.display = 'none';
      autoSettings.style.display = 'flex';
      autoControls.style.display = 'flex';
      btnProcess.disabled = false;
    });
    
    document.getElementById('btnModeManual').addEventListener('click', function() {
      currentMode = 'manual';
      modeSelect.style.display = 'none';
      manualArea.style.display = 'block';
      manualStatus.textContent = 'サムネイル読み込み中...';
      loadManualGrid();
    });
    
    /* === ユーティリティ === */
    function addLog(msg, cls) {
      logArea.style.display = 'block';
      var div = document.createElement('div');
      if (cls) div.className = cls;
      div.textContent = '[' + new Date().toLocaleTimeString() + '] ' + msg;
      logArea.appendChild(div);
      logArea.scrollTop = logArea.scrollHeight;
    }
    function setProgress(pct, msg) {
      progressFill.style.width = pct + '%';
      progressText.textContent = Math.round(pct) + '%';
      if (msg) statusText.textContent = msg;
    }
    
    function pageToImage(pdfDoc, pageNum, dpi) {
      return pdfDoc.getPage(pageNum).then(function(page) {
        var vp = page.getViewport({ scale: dpi / 72 });
        var canvas = document.createElement('canvas');
        canvas.width = vp.width; canvas.height = vp.height;
        return page.render({ canvasContext: canvas.getContext('2d'), viewport: vp }).promise.then(function() { return canvas; });
      });
    }
    
    function rotateCanvas(canvas, degrees) {
      var d = ((degrees % 360) + 360) % 360;
      if (d === 0) return canvas;
      var c2 = document.createElement('canvas');
      var ctx = c2.getContext('2d');
      if (d === 90 || d === 270) { c2.width = canvas.height; c2.height = canvas.width; }
      else { c2.width = canvas.width; c2.height = canvas.height; }
      ctx.translate(c2.width / 2, c2.height / 2);
      ctx.rotate(d * Math.PI / 180);
      ctx.drawImage(canvas, -canvas.width / 2, -canvas.height / 2);
      return c2;
    }
    
    function canvasToThumbSrc(canvas, w) {
      var s = w / canvas.width;
      var tc = document.createElement('canvas');
      tc.width = w; tc.height = canvas.height * s;
      tc.getContext('2d').drawImage(canvas, 0, 0, tc.width, tc.height);
      return tc.toDataURL('image/jpeg', 0.6);
    }
    
    function detectOrientation(canvas, worker) {
      var angles = [0, 90, 180, 270];
      var bestAngle = 0; var bestConf = -1; var idx = 0;
      function next() {
        if (idx >= angles.length) return Promise.resolve({ angle: bestAngle, confidence: bestConf });
        var angle = angles[idx];
        var rotated = rotateCanvas(canvas, angle);
        var dataUrl = rotated.toDataURL('image/png');
        return worker.recognize(dataUrl).then(function(ret) {
          var conf = ret.data.confidence;
          if (conf > bestConf) { bestConf = conf; bestAngle = angle; }
          if (angle === 0 && conf >= 85) return { angle: 0, confidence: conf };
          idx++; return next();
        }).catch(function() { idx++; return next(); });
      }
      return next();
    }
    
    /* === 手動モード === */
    async function loadManualGrid() {
      manualGrid.innerHTML = '';
      var pdfDoc = await pdfjsLib.getDocument({ data: pdfArrayBuffer.slice(0) }).promise;
      var total = pdfDoc.numPages;
      pageData = [];
    
      for (var p = 1; p <= total; p++) {
        var cv = await pageToImage(pdfDoc, p, 100);
        pageData.push({ canvas: cv, autoAngle: 0, manualAngle: 0,
          totalAngle: function() { return ((this.autoAngle + this.manualAngle) % 360 + 360) % 360; }
        });
        addMgCard(p - 1, cv);
        manualStatus.textContent = '読み込み中: ' + p + '/' + total;
      }
      manualStatus.textContent = total + 'ページ読み込み完了 — サムネイルをクリックして回転';
    }
    
    function addMgCard(idx, canvas) {
      var card = document.createElement('div');
      card.className = 'mg-card'; card.id = 'mgcard-' + idx;
      var img = document.createElement('img');
      img.src = canvasToThumbSrc(canvas, 300);
      img.id = 'mgimg-' + idx;
      card.appendChild(img);
      var label = document.createElement('div');
      label.className = 'mg-label'; label.textContent = 'P.' + (idx + 1);
      card.appendChild(label);
      var info = document.createElement('div');
      info.className = 'mg-info'; info.id = 'mginfo-' + idx; info.textContent = '0°';
      card.appendChild(info);
      var btn = document.createElement('button');
      btn.textContent = '90° 回転';
      btn.style.cssText = 'padding:6px 16px;border:none;border-radius:6px;cursor:pointer;font-weight:bold;background:#ffaa00;color:#1a1a2e;margin-top:6px;';
      btn.addEventListener('click', function() {
        pageData[idx].manualAngle = (pageData[idx].manualAngle + 90) % 360;
        refreshMgCard(idx);
      });
      card.appendChild(btn);
      manualGrid.appendChild(card);
    }
    
    function refreshMgCard(idx) {
      var pd = pageData[idx];
      var total = pd.totalAngle();
      var rotated = rotateCanvas(pd.canvas, total);
      document.getElementById('mgimg-' + idx).src = canvasToThumbSrc(rotated, 300);
      document.getElementById('mginfo-' + idx).textContent = total === 0 ? '0°' : total + '°';
    }
    
    /* 手動DL */
    document.getElementById('btnManualDL').addEventListener('click', function() { generateAndDownload(); });
    document.getElementById('btnManualReset').addEventListener('click', function() { fullReset(); });
    
    /* === 自動モード === */
    btnProcess.addEventListener('click', async function() {
      if (!pdfArrayBuffer) return;
      btnProcess.disabled = true;
      btnDownload.style.display = 'none'; btnReset.style.display = 'none';
      reviewArea.style.display = 'none'; thumbStrip.innerHTML = '';
      logArea.innerHTML = ''; progressArea.style.display = 'block';
      setProgress(0, '初期化中...'); pageData = []; currentPage = 0;
    
      var startTime = Date.now();
      var dpi = parseInt(document.getElementById('dpiSelect').value);
      var numWorkers = parseInt(document.getElementById('workerCount').value);
      addLog('DPI: ' + dpi + ' / Worker数: ' + numWorkers);
    
      try {
        var pdfDoc = await pdfjsLib.getDocument({ data: pdfArrayBuffer.slice(0) }).promise;
        var totalPages = pdfDoc.numPages;
        addLog('総ページ数: ' + totalPages);
    
        setProgress(5, 'OCRエンジン初期化中(初回は学習データDLで時間かかります)...');
        var workers = [];
        for (var i = 0; i < numWorkers; i++) {
          var w = await Tesseract.createWorker('jpn+eng', 1, { logger: function() {} });
          workers.push(w);
        }
        addLog('OCRエンジン ' + numWorkers + '基 準備完了');
    
        setProgress(10, 'ページ画像化中...');
        for (var p = 1; p <= totalPages; p++) {
          var cv = await pageToImage(pdfDoc, p, dpi);
          pageData.push({ canvas: cv, autoAngle: 0, manualAngle: 0, confidence: 0,
            totalAngle: function() { return ((this.autoAngle + this.manualAngle) % 360 + 360) % 360; }
          });
          setProgress(10 + (20 * p / totalPages), '画像化中: ' + p + '/' + totalPages);
        }
        addLog('全ページ画像化完了');
    
        setProgress(30, '文字方向検出中...');
        var processed = 0; var nextPageIdx = 0;
    
        async function processPage(pageIdx, worker) {
          var result = await detectOrientation(pageData[pageIdx].canvas, worker);
          pageData[pageIdx].autoAngle = result.angle;
          pageData[pageIdx].confidence = result.confidence;
          processed++;
          setProgress(30 + (60 * processed / totalPages), '向き検出中: ' + processed + '/' + totalPages);
          var pn = pageIdx + 1;
          if (result.angle === 0) {
            addLog('P.' + pn + ': 正立(信頼度 ' + Math.round(result.confidence) + '%)', 'log-ok');
          } else {
            addLog('P.' + pn + ': ' + result.angle + '° 回転(信頼度 ' + Math.round(result.confidence) + '%)', 'log-rotate');
          }
        }
        async function workerLoop(worker) {
          while (nextPageIdx < totalPages) { var idx = nextPageIdx++; await processPage(idx, worker); }
        }
        await Promise.all(workers.map(function(w) { return workerLoop(w); }));
    
        for (var wi = 0; wi < workers.length; wi++) await workers[wi].terminate();
        addLog('OCRエンジン終了');
    
        setProgress(100, '完了');
        var elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
        var rotatedCount = pageData.filter(function(d) { return d.autoAngle !== 0; }).length;
        timeInfo.textContent = '処理時間: ' + elapsed + '秒 / ' + totalPages + 'ページ / ' + rotatedCount + 'ページ自動回転';
        addLog('処理完了: ' + elapsed + '秒', 'log-ok');
    
        /* 自動DL */
        if (document.getElementById('chkAutoDL').checked) {
          await generateAndDownload();
          addLog('自動ダウンロード完了', 'log-ok');
        }
    
        /* レビュー表示 */
        reviewArea.style.display = 'block';
        buildThumbStrip();
        renderPreview(0);
    
        btnDownload.style.display = 'inline-block';
        btnReset.style.display = 'inline-block';
        btnProcess.disabled = false;
    
      } catch (err) {
        addLog('エラー: ' + err.message, 'log-err');
        setProgress(0, 'エラーが発生しました');
        btnProcess.disabled = false; btnReset.style.display = 'inline-block';
      }
    });
    
    /* === レビュー === */
    function renderPreview(idx) {
      if (idx < 0 || idx >= pageData.length) return;
      currentPage = idx;
      var pd = pageData[idx];
      var total = pd.totalAngle();
      var rotated = rotateCanvas(pd.canvas, total);
      previewCanvas.width = rotated.width; previewCanvas.height = rotated.height;
      previewCanvas.getContext('2d').drawImage(rotated, 0, 0);
    
      var autoLabel = pd.autoAngle === 0 ? '自動: 正立' : '自動: ' + pd.autoAngle + '°';
      var manualLabel = pd.manualAngle === 0 ? '' : ' / 手動: ' + pd.manualAngle + '°';
      previewLabel.textContent = 'P.' + (idx + 1) + ' — ' + autoLabel + manualLabel;
    
      rotValue.textContent = total + '°';
      rotSource.textContent = pd.manualAngle !== 0 ? '(手動調整あり)' : pd.autoAngle !== 0 ? '(自動検出)' : '';
    
      pageIndicator.textContent = (idx + 1) + ' / ' + pageData.length;
      document.getElementById('btnPrev').disabled = (idx === 0);
      document.getElementById('btnNext').disabled = (idx === pageData.length - 1);
    
      var items = thumbStrip.querySelectorAll('.ts-item');
      for (var i = 0; i < items.length; i++) items[i].classList.toggle('active', i === idx);
      if (items[idx]) items[idx].scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' });
    }
    
    function buildThumbStrip() {
      thumbStrip.innerHTML = '';
      for (var i = 0; i < pageData.length; i++) {
        (function(idx) {
          var pd = pageData[idx];
          var total = pd.totalAngle();
          var rotated = rotateCanvas(pd.canvas, total);
          var item = document.createElement('div');
          item.className = 'ts-item' + (idx === currentPage ? ' active' : '') + (total !== 0 ? ' modified' : '');
          var img = document.createElement('img');
          img.src = canvasToThumbSrc(rotated, 70);
          item.appendChild(img);
          var label = document.createElement('div');
          label.className = 'ts-label'; label.textContent = 'P.' + (idx + 1);
          item.appendChild(label);
          var badge = document.createElement('span');
          badge.className = 'ts-badge';
          if (total === 0) { badge.textContent = '正立'; badge.style.background = '#00ff88'; badge.style.color = '#1a1a2e'; }
          else { badge.textContent = total + '°'; badge.style.background = '#ffaa00'; badge.style.color = '#1a1a2e'; }
          item.appendChild(badge);
          item.addEventListener('click', function() { renderPreview(idx); });
          thumbStrip.appendChild(item);
        })(i);
      }
    }
    
    function updateThumbBadge(idx) {
      var pd = pageData[idx];
      var total = pd.totalAngle();
      var item = thumbStrip.children[idx]; if (!item) return;
      item.classList.toggle('modified', total !== 0);
      var badge = item.querySelector('.ts-badge');
      if (total === 0) { badge.textContent = '正立'; badge.style.background = '#00ff88'; badge.style.color = '#1a1a2e'; }
      else { badge.textContent = total + '°'; badge.style.background = '#ffaa00'; badge.style.color = '#1a1a2e'; }
      var rotated = rotateCanvas(pd.canvas, total);
      item.querySelector('img').src = canvasToThumbSrc(rotated, 70);
    }
    
    /* 手動回転(レビュー内) */
    function applyManualRotation(delta) {
      var pd = pageData[currentPage];
      if (delta === null) { pd.manualAngle = -pd.autoAngle; }
      else { pd.manualAngle = ((pd.manualAngle + delta) % 360 + 360) % 360; }
      updateThumbBadge(currentPage);
      renderPreview(currentPage);
    }
    
    document.getElementById('btnRotCW').addEventListener('click', function() { applyManualRotation(90); });
    document.getElementById('btnRotCCW').addEventListener('click', function() { applyManualRotation(-90); });
    document.getElementById('btnRot180').addEventListener('click', function() { applyManualRotation(180); });
    document.getElementById('btnRotReset').addEventListener('click', function() {
      pageData[currentPage].manualAngle = 0; pageData[currentPage].autoAngle = 0;
      updateThumbBadge(currentPage); renderPreview(currentPage);
    });
    
    document.getElementById('btnPrev').addEventListener('click', function() { if (currentPage > 0) renderPreview(currentPage - 1); });
    document.getElementById('btnNext').addEventListener('click', function() { if (currentPage < pageData.length - 1) renderPreview(currentPage + 1); });
    
    document.addEventListener('keydown', function(e) {
      if (reviewArea.style.display === 'none') return;
      if (e.key === 'ArrowLeft') { if (currentPage > 0) renderPreview(currentPage - 1); e.preventDefault(); }
      if (e.key === 'ArrowRight') { if (currentPage < pageData.length - 1) renderPreview(currentPage + 1); e.preventDefault(); }
      if (e.key === 'r' || e.key === 'R') { applyManualRotation(90); e.preventDefault(); }
      if (e.key === 'l' || e.key === 'L') { applyManualRotation(-90); e.preventDefault(); }
    });
    
    /* リセット */
    btnReset.addEventListener('click', function() { fullReset(); });
    
    /* === PDF生成&ダウンロード === */
    async function generateAndDownload() {
      var pdfLibDoc = await PDFLib.PDFDocument.load(pdfArrayBuffer);
      var pages = pdfLibDoc.getPages();
      var rotatedCount = 0;
      for (var i = 0; i < pageData.length; i++) {
        var total = pageData[i].totalAngle();
        if (total !== 0) {
          var cur = pages[i].getRotation().angle;
          pages[i].setRotation(PDFLib.degrees(cur + total));
          rotatedCount++;
        }
      }
      var bytes = await pdfLibDoc.save();
      var blob = new Blob([bytes], { type: 'application/pdf' });
      var url = URL.createObjectURL(blob);
      var a = document.createElement('a');
      a.href = url; a.download = originalFileName + '_rotated.pdf';
      document.body.appendChild(a); a.click(); document.body.removeChild(a);
      setTimeout(function() { URL.revokeObjectURL(url); }, 5000);
      addLog('PDF保存: ' + rotatedCount + 'ページ回転 → ' + originalFileName + '_rotated.pdf', 'log-ok');
    }
    </script>
    </body>
    </html>
    

    ③ PDF変換

    <!DOCTYPE html>
    <html lang="ja">
    <head>
    <style>
      .preloader {
        position: fixed;
        top: 0; left: 0; width: 100%; height: 100%;
        background: #fff;
        display: flex; justify-content: center; align-items: center; gap: 20px;
        z-index: 9999;
        animation: fadeOut 0.1s 0.3s forwards;
      }
      .glasses {
        display: flex; align-items: center;
        animation: swing 0.3s ease-in-out infinite alternate;
        transform-origin: top center;
      }
      .lens { width: 40px; height: 20px; border: 3px solid #888; border-radius: 4px; background: rgba(200,200,200,0.2); }
      .bridge { width: 12px; height: 3px; background: #888; }
      .temple { width: 20px; height: 3px; background: #888; }
      .pencil {
        font-size: 48px;
        animation: swing 0.3s ease-in-out infinite alternate;
        animation-delay: 0.15s;
        transform-origin: top center;
        display: inline-block;
      }
      @keyframes swing { 0% { transform: rotate(-15deg); } 100% { transform: rotate(15deg); } }
      @keyframes fadeOut { to { opacity: 0; visibility: hidden; } }
    </style>
    <div class="preloader">
      <div class="glasses">
        <div class="temple"></div><div class="lens"></div><div class="bridge"></div><div class="lens"></div><div class="temple"></div>
      </div>
      <span class="pencil">✏️</span>
    </div>
    
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>画像形式変換ツール(PDF対応)</title>
    
        <style>
            * { margin: 0; padding: 0; box-sizing: border-box; }
            body {
                font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
                background: linear-gradient(135deg, #667eea 0%,  #764ba2 100%);
                min-height: 100vh;
                display: flex; justify-content: center; align-items: center; padding: 20px;
            }
            .container {
                background: white; border-radius: 20px;
                box-shadow: 0 20px 60px rgba(0,0,0,0.3);
                padding: 40px; max-width: 700px; width: 100%;
            }
            h1 { text-align: center; color: #333; margin-bottom: 30px; font-size: 28px; }
            .upload-area {
                border: 3px dashed #667eea; border-radius: 15px; padding: 40px;
                text-align: center; transition: all 0.3s ease; cursor: pointer; background: #f8f9fa;
            }
            .upload-area:hover { border-color: #764ba2; background: #f0f0f0; }
            .upload-area.dragover { background: #e8e8ff; border-color: #764ba2; }
            .upload-icon { font-size: 48px; color: #667eea; margin-bottom: 20px; }
            .upload-text { color: #666; font-size: 16px; margin-bottom: 10px; }
            .upload-hint { color: #999; font-size: 14px; }
            .supported-formats { margin-top: 10px; padding: 10px; background: #f0f8ff; border-radius: 8px; }
            .supported-formats-title { color: #667eea; font-weight: bold; margin-bottom: 5px; }
            .format-list { display: flex; flex-wrap: wrap; gap: 8px; justify-content: center; }
            .format-tag { background: #667eea; color: white; padding: 2px 8px; border-radius: 4px; font-size: 12px; }
            input[type="file"] { display: none; }
            .preview-section { margin-top: 30px; display: none; }
            .preview-image {
                max-width: 100%; max-height: 300px; border-radius: 10px;
                box-shadow: 0 5px 15px rgba(0,0,0,0.2); margin: 20px auto; display: block;
            }
            .file-info { background: #f8f9fa; padding: 15px; border-radius: 10px; margin-bottom: 20px; }
            .file-info p { color: #666; margin: 5px 0; font-size: 14px; }
            .format-selection { margin: 20px 0; }
            .format-selection label { display: block; color: #333; margin-bottom: 10px; font-weight: 500; }
            .format-buttons { display: flex; gap: 10px; flex-wrap: wrap; }
            .format-btn {
                flex: 1; padding: 10px 20px; border: 2px solid #667eea; background: white;
                color: #667eea; border-radius: 8px; cursor: pointer; transition: all 0.3s ease;
                font-size: 14px; font-weight: 500; min-width: 80px;
            }
            .format-btn:hover { background: #667eea; color: white; }
            .format-btn.active { background: #667eea; color: white; }
            .quality-slider, .pdf-options {
                margin: 20px 0; padding: 15px; background: #f8f9fa; border-radius: 10px;
            }
            .quality-slider label, .pdf-options label {
                display: block; color: #333; margin-bottom: 10px; font-weight: 500;
            }
            .slider-container { display: flex; align-items: center; gap: 15px; }
            input[type="range"] {
                flex: 1; -webkit-appearance: none; appearance: none;
                height: 6px; background: #ddd; border-radius: 3px; outline: none;
            }
            input[type="range"]::-webkit-slider-thumb {
                -webkit-appearance: none; appearance: none;
                width: 20px; height: 20px; background: #667eea; border-radius: 50%; cursor: pointer;
            }
            input[type="range"]::-moz-range-thumb {
                width: 20px; height: 20px; background: #667eea; border-radius: 50%; cursor: pointer; border: none;
            }
            .quality-value {
                background: #667eea; color: white; padding: 5px 10px;
                border-radius: 5px; min-width: 45px; text-align: center; font-weight: 500;
            }
            .pdf-size-select {
                width: 100%; padding: 8px 12px; border: 2px solid #ddd; border-radius: 8px;
                font-size: 14px; margin-top: 10px; background: white; cursor: pointer; transition: border-color 0.3s;
            }
            .pdf-size-select:focus { outline: none; border-color: #667eea; }
            .pdf-orientation { display: flex; gap: 15px; margin-top: 10px; }
            .orientation-option { display: flex; align-items: center; gap: 5px; cursor: pointer; }
            .orientation-option input[type="radio"] { cursor: pointer; }
            .action-buttons { display: flex; gap: 10px; margin-top: 20px; }
            .btn {
                flex: 1; padding: 12px 24px; border: none; border-radius: 8px;
                font-size: 16px; font-weight: 500; cursor: pointer; transition: all 0.3s ease;
            }
            .btn-convert { background: linear-gradient(135deg, #667eea 0%,  #764ba2 100%); color: white; }
            .btn-convert:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(102,126,234,0.4); }
            .btn-convert:disabled { opacity: 0.5; cursor: not-allowed; }
            .btn-reset { background: #e0e0e0; color: #666; }
            .btn-reset:hover { background: #d0d0d0; }
            .download-link {
                display: inline-block; margin-top: 20px; padding: 12px 24px;
                background: #28a745; color: white; text-decoration: none; border-radius: 8px;
                font-weight: 500; transition: all 0.3s ease;
            }
            .download-link:hover { background: #218838; transform: translateY(-2px); box-shadow: 0 5px 15px rgba(40,167,69,0.4); }
            .bulk-download-btn {
                display: inline-block; margin-bottom: 15px; padding: 14px 28px;
                background: linear-gradient(135deg, #ff6b6b 0%, #ee5a24 100%);
                color: white; border: none; border-radius: 8px; cursor: pointer;
                font-size: 16px; font-weight: 600; transition: all 0.3s ease; width: 100%; text-align: center;
            }
            .bulk-download-btn:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(238,90,36,0.4); }
            .status-message { padding: 10px 15px; border-radius: 8px; margin-top: 15px; text-align: center; font-size: 14px; }
            .status-success { background: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
            .status-error { background: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
            .status-warning { background: #fff3cd; color: #856404; border: 1px solid #ffeaa7; }
            .status-info { background: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
            .multiple-files-area {
                margin-top: 15px; padding: 15px; background: #f0f0ff; border-radius: 10px; display: none;
            }
            .file-list { margin-top: 10px; }
            .file-item {
                display: flex; align-items: center; justify-content: space-between;
                padding: 8px; background: white; border-radius: 5px; margin-bottom: 5px;
            }
            .file-item button {
                padding: 4px 8px; background: #dc3545; color: white;
                border: none; border-radius: 4px; cursor: pointer; font-size: 12px;
            }
            .file-item button:hover { background: #c82333; }
            .loading-overlay {
                position: fixed; top: 0; left: 0; width: 100%; height: 100%;
                background: rgba(0,0,0,0.7); display: none;
                justify-content: center; align-items: center; z-index: 9999;
            }
            .loading-content { background: white; padding: 30px; border-radius: 10px; text-align: center; }
            .loading-spinner {
                border: 4px solid #f3f3f3; border-top: 4px solid #667eea;
                border-radius: 50%; width: 50px; height: 50px;
                animation: spin 1s linear infinite; margin: 0 auto 20px;
            }
            @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
            .loading-text { color: #333; font-size: 16px; }
        </style>
    </head>
    <body>
        <div class="container">
            <h1>🖼️ 画像形式変換ツール</h1>
    
            <div class="upload-area" id="uploadArea">
                <div class="upload-icon">📁</div>
                <p class="upload-text">画像をドラッグ&ドロップ</p>
                <p class="upload-hint">または クリックしてファイルを選択</p>
                <div class="supported-formats">
                    <div class="supported-formats-title">対応形式:</div>
                    <div class="format-list">
                        <span class="format-tag">JPG</span>
                        <span class="format-tag">PNG</span>
                        <span class="format-tag">BMP</span>
                        <span class="format-tag">WEBP</span>
                        <span class="format-tag">ICO</span>
                        <span class="format-tag">HEIC</span>
                        <span class="format-tag">RAW</span>
                        <span class="format-tag">PDF</span>
                    </div>
                </div>
                <input type="file" id="fileInput" accept=".jpg,.jpeg,.png,.bmp,.webp,.ico,.heic,.heif,.raw,.cr2,.nef,.arw,.dng,.orf,.rw2,.pdf,image/*,application/pdf" multiple>
            </div>
    
            <div class="multiple-files-area" id="multipleFilesArea">
                <p><strong>複数の画像を選択しました(PDF作成用)</strong></p>
                <div class="file-list" id="fileList"></div>
            </div>
    
            <div class="preview-section" id="previewSection">
                <div class="file-info" id="fileInfo"></div>
                <img class="preview-image" id="previewImage" alt="プレビュー">
    
                <div class="format-selection">
                    <label>変換後の形式を選択:</label>
                    <div class="format-buttons">
                        <button class="format-btn" data-format="jpeg">JPEG</button>
                        <button class="format-btn" data-format="png">PNG</button>
                        <button class="format-btn" data-format="bmp">BMP</button>
                        <button class="format-btn" data-format="webp">WEBP</button>
                        <button class="format-btn" data-format="ico">ICO</button>
                        <button class="format-btn" data-format="pdf">PDF</button>
                    </div>
                </div>
    
                <div class="quality-slider" id="qualitySlider">
                    <label>画質 (JPEGのみ):</label>
                    <div class="slider-container">
                        <input type="range" id="quality" min="10" max="100" value="90">
                        <span class="quality-value" id="qualityValue">90%</span>
                    </div>
                </div>
    
                <div class="pdf-options" id="pdfOptions" style="display: none;">
                    <label>PDFページサイズ:</label>
                    <select class="pdf-size-select" id="pdfPageSize">
                        <option value="a4">A4 (210 × 297 mm)</option>
                        <option value="a3">A3 (297 × 420 mm)</option>
                        <option value="letter">Letter (216 × 279 mm)</option>
                        <option value="legal">Legal (216 × 356 mm)</option>
                        <option value="fit">画像に合わせる</option>
                    </select>
                    <label style="margin-top: 15px;">用紙の向き:</label>
                    <div class="pdf-orientation">
                        <label class="orientation-option">
                            <input type="radio" name="orientation" value="portrait" checked><span>縦向き</span>
                        </label>
                        <label class="orientation-option">
                            <input type="radio" name="orientation" value="landscape"><span>横向き</span>
                        </label>
                    </div>
                </div>
    
                <div class="action-buttons">
                    <button class="btn btn-convert" id="convertBtn">変換する</button>
                    <button class="btn btn-reset" id="resetBtn">リセット</button>
                </div>
    
                <div id="downloadSection"></div>
                <div id="statusMessage"></div>
            </div>
        </div>
    
        <div class="loading-overlay" id="loadingOverlay">
            <div class="loading-content">
                <div class="loading-spinner"></div>
                <div class="loading-text" id="loadingText">変換処理中...</div>
            </div>
        </div>
    
        <!-- ライブラリ -->
        <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
        <script>
          pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
        </script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
        <script src="https://cdn.jsdelivr.net/npm/heic2any@0.0.4/dist/heic2any.min.js"></script>
    
        <script>
            let selectedFiles = [];
            let selectedFormat = 'jpeg';
            let originalImages = [];
            let isPdfSource = false;
    
            const uploadArea = document.getElementById('uploadArea');
            const fileInput = document.getElementById('fileInput');
            const previewSection = document.getElementById('previewSection');
            const previewImage = document.getElementById('previewImage');
            const fileInfo = document.getElementById('fileInfo');
            const formatButtons = document.querySelectorAll('.format-btn');
            const qualitySlider = document.getElementById('qualitySlider');
            const pdfOptions = document.getElementById('pdfOptions');
            const quality = document.getElementById('quality');
            const qualityValue = document.getElementById('qualityValue');
            const convertBtn = document.getElementById('convertBtn');
            const resetBtn = document.getElementById('resetBtn');
            const downloadSection = document.getElementById('downloadSection');
            const statusMessage = document.getElementById('statusMessage');
            const multipleFilesArea = document.getElementById('multipleFilesArea');
            const fileList = document.getElementById('fileList');
            const loadingOverlay = document.getElementById('loadingOverlay');
            const loadingText = document.getElementById('loadingText');
    
            const rawExtensions = ['cr2','nef','arw','dng','orf','rw2','raw','raf','sr2','pef'];
    
            uploadArea.addEventListener('click', () => fileInput.click());
            fileInput.addEventListener('change', (e) => handleFileSelect(Array.from(e.target.files)));
    
            uploadArea.addEventListener('dragover', (e) => { e.preventDefault(); uploadArea.classList.add('dragover'); });
            uploadArea.addEventListener('dragleave', () => uploadArea.classList.remove('dragover'));
            uploadArea.addEventListener('drop', (e) => {
                e.preventDefault(); uploadArea.classList.remove('dragover');
                handleFileSelect(Array.from(e.dataTransfer.files));
            });
    
            function showLoading(show = true, text = '変換処理中...') {
                loadingText.textContent = text;
                loadingOverlay.style.display = show ? 'flex' : 'none';
            }
    
            function getFileFormat(file) {
                const ext = file.name.split('.').pop().toLowerCase();
                if (ext === 'ico') return 'ico';
                if (['heic','heif'].includes(ext)) return 'heic';
                if (rawExtensions.includes(ext)) return 'raw';
                if (ext === 'pdf' || file.type === 'application/pdf') return 'pdf';
                if (file.type.startsWith('image/')) return 'image';
                return 'unknown';
            }
    
            async function handleFileSelect(files) {
                if (!files || files.length === 0) return;
                selectedFiles = []; originalImages = []; isPdfSource = false;
    
                const validFiles = files.filter(f => getFileFormat(f) !== 'unknown');
                if (validFiles.length === 0) { showStatus('対応している画像ファイルを選択してください','error'); return; }
    
                selectedFiles = validFiles;
                const hasSpecial = validFiles.some(f => { const fmt = getFileFormat(f); return fmt === 'heic' || fmt === 'raw'; });
                if (hasSpecial) showStatus('HEIC/RAWファイルの処理には時間がかかる場合があります','info');
    
                if (validFiles.length > 1) {
                    showMultipleFiles(validFiles);
                    await loadImagesForPDF(validFiles);
                } else {
                    multipleFilesArea.style.display = 'none';
                    await loadSingleImage(validFiles[0]);
                }
                previewSection.style.display = 'block';
                uploadArea.style.display = 'none';
            }
    
            async function convertHEIC(file) {
                try {
                    showLoading(true, 'HEIC変換中...');
                    const blob = await heic2any({ blob: file, toType: 'image/jpeg', quality: 0.9 });
                    return blob;
                } catch (error) {
                    console.error('HEIC変換エラー:', error);
                    showStatus('HEICファイルの変換に失敗しました','error');
                    return null;
                } finally { showLoading(false); }
            }
    
            async function processRAW(file) {
                showStatus('RAWファイルは限定的なサポートです。正確な変換には専用ソフトをご利用ください','warning');
                return file;
            }
    
            function showMultipleFiles(files) {
                multipleFilesArea.style.display = 'block';
                fileList.innerHTML = '';
                files.forEach((file, index) => {
                    const format = getFileFormat(file);
                    const label = format === 'heic' ? ' (HEIC)' : format === 'raw' ? ' (RAW)' : format === 'ico' ? ' (ICO)' : format === 'pdf' ? ' (PDF)' : '';
                    const item = document.createElement('div');
                    item.className = 'file-item';
                    item.innerHTML = `<span>${index+1}. ${file.name}${label}</span><button onclick="removeFile(${index})">削除</button>`;
                    fileList.appendChild(item);
                });
            }
    
            window.removeFile = function(index) {
                selectedFiles.splice(index, 1);
                originalImages.splice(index, 1);
                if (selectedFiles.length === 0) { resetAll(); }
                else if (selectedFiles.length === 1) { multipleFilesArea.style.display = 'none'; loadSingleImage(selectedFiles[0]); }
                else { showMultipleFiles(selectedFiles); if (originalImages.length > 0) previewImage.src = originalImages[0].src; }
            }
    
            async function loadSingleImage(file) {
                try {
                    showLoading(true, '読み込み中...');
                    const format = getFileFormat(file);
    
                    if (format === 'pdf') {
                        isPdfSource = true;
                        const arrayBuffer = await file.arrayBuffer();
                        const pdfDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
                        const totalPages = pdfDoc.numPages;
                        originalImages = []; selectedFiles = [file];
    
                        for (let i = 1; i <= totalPages; i++) {
                            loadingText.textContent = `PDFページ読み込み中... ${i}/${totalPages}`;
                            const page = await pdfDoc.getPage(i);
                            const scale = 2;
                            const viewport = page.getViewport({ scale });
                            const canvas = document.createElement('canvas');
                            canvas.width = viewport.width; canvas.height = viewport.height;
                            const ctx = canvas.getContext('2d');
                            await page.render({ canvasContext: ctx, viewport }).promise;
                            const img = new Image();
                            await new Promise((resolve) => { img.onload = resolve; img.src = canvas.toDataURL('image/png'); });
                            originalImages.push(img);
                        }
                        previewImage.src = originalImages[0].src;
                        const fileSize = (file.size / 1024).toFixed(2);
                        fileInfo.innerHTML = `
                            <p><strong>ファイル名:</strong> ${file.name}</p>
                            <p><strong>形式:</strong> PDF</p>
                            <p><strong>サイズ:</strong> ${fileSize} KB</p>
                            <p><strong>ページ数:</strong> ${totalPages} ページ</p>
                            <p><strong>解像度(1P):</strong> ${originalImages[0].width} × ${originalImages[0].height} px</p>`;
                        if (totalPages > 1) showStatus(`PDF ${totalPages}ページを読み込みました。各ページが個別ファイルになります`,'info');
                        showLoading(false);
                        return;
                    }
    
                    isPdfSource = false;
                    let processedFile = file;
                    if (format === 'heic') {
                        const convertedBlob = await convertHEIC(file);
                        if (!convertedBlob) { showLoading(false); return; }
                        processedFile = new File([convertedBlob], file.name.replace(/\.heic$/i, '.jpg'), { type: 'image/jpeg' });
                    }
                    if (format === 'raw') processedFile = await processRAW(file);
    
                    const reader = new FileReader();
                    reader.onload = (e) => {
                        const img = new Image();
                        img.onload = () => { originalImages = [img]; previewImage.src = e.target.result; showFileInfo(file, img, format); showLoading(false); };
                        img.onerror = () => { showStatus('画像の読み込みに失敗しました','error'); showLoading(false); };
                        img.src = e.target.result;
                    };
                    reader.readAsDataURL(processedFile);
                } catch (error) {
                    console.error('読み込みエラー:', error);
                    showStatus('ファイルの読み込みに失敗しました','error');
                    showLoading(false);
                }
            }
    
            async function loadImagesForPDF(files) {
                let loadedCount = 0;
                originalImages = new Array(files.length);
                isPdfSource = false;
                showLoading(true, '複数ファイル読み込み中...');
    
                for (let index = 0; index < files.length; index++) {
                    const file = files[index];
                    let processedFile = file;
                    const format = getFileFormat(file);
                    try {
                        if (format === 'heic') {
                            const convertedBlob = await convertHEIC(file);
                            if (!convertedBlob) { loadedCount++; continue; }
                            processedFile = new File([convertedBlob], file.name.replace(/\.heic$/i, '.jpg'), { type: 'image/jpeg' });
                        }
                        if (format === 'raw') processedFile = await processRAW(file);
    
                        if (format === 'pdf') {
                            const arrayBuffer = await file.arrayBuffer();
                            const pdfDoc = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
                            const page = await pdfDoc.getPage(1);
                            const scale = 2;
                            const viewport = page.getViewport({ scale });
                            const canvas = document.createElement('canvas');
                            canvas.width = viewport.width; canvas.height = viewport.height;
                            const ctx = canvas.getContext('2d');
                            await page.render({ canvasContext: ctx, viewport }).promise;
                            const img = new Image();
                            await new Promise((resolve) => { img.onload = resolve; img.src = canvas.toDataURL('image/png'); });
                            originalImages[index] = img;
                            loadedCount++;
                            if (index === 0) { previewImage.src = img.src; showFileInfo(file, img, format); }
                            if (loadedCount === files.length) fileInfo.innerHTML += `<p><strong>総ファイル数:</strong> ${files.length} 枚</p>`;
                            continue;
                        }
    
                        await new Promise((resolve) => {
                            const reader = new FileReader();
                            reader.onload = (e) => {
                                const img = new Image();
                                img.onload = () => {
                                    originalImages[index] = img; loadedCount++;
                                    if (index === 0) { previewImage.src = e.target.result; showFileInfo(file, img, format); }
                                    if (loadedCount === files.length) fileInfo.innerHTML += `<p><strong>総ファイル数:</strong> ${files.length} 枚</p>`;
                                    resolve();
                                };
                                img.src = e.target.result;
                            };
                            reader.readAsDataURL(processedFile);
                        });
                    } catch (error) {
                        console.error(`ファイル ${file.name} の処理エラー:`, error);
                        loadedCount++;
                    }
                }
                showLoading(false);
            }
    
            function showFileInfo(file, img, format) {
                const fileSize = (file.size / 1024).toFixed(2);
                let fileType = file.type.split('/')[1]?.toUpperCase() || 'Unknown';
                if (format === 'heic') fileType = 'HEIC';
                if (format === 'raw') fileType = 'RAW (' + file.name.split('.').pop().toUpperCase() + ')';
                if (format === 'ico') fileType = 'ICO';
                if (format === 'pdf') fileType = 'PDF';
                fileInfo.innerHTML = `
                    <p><strong>ファイル名:</strong> ${file.name}</p>
                    <p><strong>形式:</strong> ${fileType}</p>
                    <p><strong>サイズ:</strong> ${fileSize} KB</p>
                    ${img ? `<p><strong>解像度:</strong> ${img.width} × ${img.height} px</p>` : ''}`;
            }
    
            formatButtons.forEach(btn => {
                btn.addEventListener('click', () => {
                    formatButtons.forEach(b => b.classList.remove('active'));
                    btn.classList.add('active');
                    selectedFormat = btn.dataset.format;
                    qualitySlider.style.display = selectedFormat === 'jpeg' ? 'block' : 'none';
                    pdfOptions.style.display = selectedFormat === 'pdf' ? 'block' : 'none';
                });
            });
            formatButtons[0].classList.add('active');
    
            quality.addEventListener('input', () => { qualityValue.textContent = quality.value + '%'; });
    
            convertBtn.addEventListener('click', () => {
                if (originalImages.length === 0) return;
                if (selectedFormat === 'pdf') convertToPDF();
                else if (selectedFormat === 'ico') convertToICO();
                else convertToImage();
            });
    
            function convertToICO() {
                showLoading(true);
                if (originalImages.length > 1) showStatus('ICO変換は1ページ目のみ対象です','warning');
                setTimeout(() => {
                    const img = originalImages[0];
                    const sizes = [16,32,48,64,128,256];
                    let targetSize = 32;
                    for (let size of sizes) { if (img.width <= size && img.height <= size) { targetSize = size; break; } targetSize = size; }
                    const canvas = document.createElement('canvas');
                    canvas.width = targetSize; canvas.height = targetSize;
                    const ctx = canvas.getContext('2d');
                    ctx.clearRect(0, 0, targetSize, targetSize);
                    const scale = Math.min(targetSize / img.width, targetSize / img.height);
                    const sw = img.width * scale; const sh = img.height * scale;
                    const x = (targetSize - sw) / 2; const y = (targetSize - sh) / 2;
                    ctx.drawImage(img, x, y, sw, sh);
                    canvas.toBlob((blob) => {
                        if (!blob) { showStatus('ICO変換に失敗しました','error'); showLoading(false); return; }
                        const url = URL.createObjectURL(blob);
                        const originalName = selectedFiles[0].name.split('.')[0];
                        downloadSection.innerHTML = `
                            <a href="${url}" download="${originalName}_converted.ico" class="download-link">📥 ICOファイルをダウンロード</a>
                            <p style="margin-top:10px;color:#666;font-size:12px;">※ ICOサイズ: ${targetSize}×${targetSize}px</p>`;
                        showStatus('ICO変換が完了しました!','success');
                        showLoading(false);
                    }, 'image/png');
                }, 100);
            }
    
            function convertToImage() {
                showLoading(true);
                setTimeout(() => {
                    let mimeType, extension;
                    switch (selectedFormat) {
                        case 'jpeg': mimeType = 'image/jpeg'; extension = 'jpg'; break;
                        case 'png':  mimeType = 'image/png';  extension = 'png'; break;
                        case 'bmp':  mimeType = 'image/bmp';  extension = 'bmp'; break;
                        case 'webp': mimeType = 'image/webp'; extension = 'webp'; break;
                    }
                    const q = selectedFormat === 'jpeg' ? quality.value / 100 : 1;
                    const originalName = selectedFiles[0].name.split('.')[0];
    
                    // 単一画像
                    if (originalImages.length === 1) {
                        const canvas = document.createElement('canvas');
                        const img = originalImages[0];
                        canvas.width = img.width; canvas.height = img.height;
                        const ctx = canvas.getContext('2d');
                        if (selectedFormat === 'jpeg' || selectedFormat === 'bmp') { ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, canvas.width, canvas.height); }
                        ctx.drawImage(img, 0, 0);
                        canvas.toBlob((blob) => {
                            if (!blob) { showStatus('変換に失敗しました','error'); showLoading(false); return; }
                            const url = URL.createObjectURL(blob);
                            downloadSection.innerHTML = `<a href="${url}" download="${originalName}_converted.${extension}" class="download-link">📥 変換した画像をダウンロード</a>`;
                            showStatus('変換が完了しました!','success');
                            showLoading(false);
                        }, mimeType, q);
                        return;
                    }
    
                    // 複数ページ
                    const total = originalImages.length;
                    let blobs = new Array(total);
                    let processed = 0;
    
                    originalImages.forEach((img, index) => {
                        const canvas = document.createElement('canvas');
                        canvas.width = img.width; canvas.height = img.height;
                        const ctx = canvas.getContext('2d');
                        if (selectedFormat === 'jpeg' || selectedFormat === 'bmp') { ctx.fillStyle = '#FFFFFF'; ctx.fillRect(0, 0, canvas.width, canvas.height); }
                        ctx.drawImage(img, 0, 0);
    
                        canvas.toBlob((blob) => {
                            blobs[index] = blob;
                            processed++;
    
                            if (processed === total) {
                                let linksHTML = '';
    
                                // 一括ダウンロードボタン(先頭)
                                if (total > 1) {
                                    linksHTML += `<button class="bulk-download-btn" id="bulkDownloadBtn">📦 全${total}ファイルを一括ダウンロード</button><hr style="margin:15px 0;">`;
                                }
    
                                linksHTML += '<p style="margin-bottom:10px;font-weight:bold;">個別ダウンロード:</p>';
    
                                const fileData = [];
                                blobs.forEach((b, i) => {
                                    if (b) {
                                        const url = URL.createObjectURL(b);
                                        const fileName = `${originalName}_page${i + 1}.${extension}`;
                                        fileData.push({ url, fileName });
                                        linksHTML += `<a href="${url}" download="${fileName}" class="download-link" style="margin:5px;font-size:14px;">📥 ${fileName}</a><br>`;
                                    }
                                });
    
                                downloadSection.innerHTML = linksHTML;
    
                                const bulkBtn = document.getElementById('bulkDownloadBtn');
                                if (bulkBtn) {
                                    bulkBtn.addEventListener('click', () => {
                                        fileData.forEach((fd, i) => {
                                            setTimeout(() => {
                                                const a = document.createElement('a');
                                                a.href = fd.url; a.download = fd.fileName;
                                                document.body.appendChild(a); a.click(); document.body.removeChild(a);
                                            }, i * 300);
                                        });
                                    });
                                }
    
                                showStatus(`${total}ページの変換が完了しました!`,'success');
                                showLoading(false);
                            }
                        }, mimeType, q);
                    });
                }, 100);
            }
    
            function convertToPDF() {
                showLoading(true);
                setTimeout(() => {
                    const { jsPDF } = window.jspdf;
                    const pageSize = document.getElementById('pdfPageSize').value;
                    const orientation = document.querySelector('input[name="orientation"]:checked').value;
                    let pdf;
    
                    if (pageSize === 'fit') {
                        const img = originalImages[0];
                        const wMM = img.width * 0.264583; const hMM = img.height * 0.264583;
                        pdf = new jsPDF({ orientation: wMM > hMM ? 'landscape' : 'portrait', unit: 'mm', format: [wMM, hMM] });
                    } else {
                        pdf = new jsPDF({ orientation, unit: 'mm', format: pageSize });
                    }
    
                    const pageWidth = pdf.internal.pageSize.getWidth();
                    const pageHeight = pdf.internal.pageSize.getHeight();
    
                    originalImages.forEach((img, index) => {
                        if (index > 0) pdf.addPage();
                        const canvas = document.createElement('canvas');
                        canvas.width = img.width; canvas.height = img.height;
                        const ctx = canvas.getContext('2d'); ctx.drawImage(img, 0, 0);
                        const imgData = canvas.toDataURL('image/jpeg', 0.95);
                        const imgRatio = img.width / img.height;
                        const pageRatio = pageWidth / pageHeight;
                        let fw, fh, x, y;
                        if (imgRatio > pageRatio) { fw = pageWidth; fh = pageWidth / imgRatio; x = 0; y = (pageHeight - fh) / 2; }
                        else { fh = pageHeight; fw = pageHeight * imgRatio; x = (pageWidth - fw) / 2; y = 0; }
                        pdf.addImage(imgData, 'JPEG', x, y, fw, fh);
                    });
    
                    const pdfBlob = pdf.output('blob');
                    const url = URL.createObjectURL(pdfBlob);
                    const originalName = selectedFiles[0].name.split('.')[0];
                    downloadSection.innerHTML = `<a href="${url}" download="${originalName}_converted.pdf" class="download-link">📄 PDFをダウンロード</a>`;
                    showStatus('PDF変換が完了しました!','success');
                    showLoading(false);
                }, 100);
            }
    
            function resetAll() {
                selectedFiles = []; selectedFormat = 'jpeg'; originalImages = []; isPdfSource = false;
                fileInput.value = '';
                previewSection.style.display = 'none'; uploadArea.style.display = 'block';
                multipleFilesArea.style.display = 'none';
                downloadSection.innerHTML = ''; statusMessage.innerHTML = '';
                formatButtons.forEach(b => b.classList.remove('active'));
                formatButtons[0].classList.add('active');
                quality.value = 90; qualityValue.textContent = '90%';
                qualitySlider.style.display = 'block'; pdfOptions.style.display = 'none';
            }
    
            resetBtn.addEventListener('click', resetAll);
    
            function showStatus(message, type) {
                statusMessage.innerHTML = `<div class="status-message status-${type}">${message}</div>`;
                setTimeout(() => { statusMessage.innerHTML = ''; }, 5000);
            }
        </script>
    </body>
    </html>
    

    ④ PDF比較

    <!DOCTYPE html>
    <html lang="ja">
    <head>
    <style>
      .preloader {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: #fff;
        display: flex;
        justify-content: center;
        align-items: center;
        gap: 20px;
        z-index: 9999;
        animation: fadeOut 0.1s 0.3s forwards;
      }
      .glasses {
        display: flex;
        align-items: center;
        animation: swing 0.3s ease-in-out infinite alternate;
        transform-origin: top center;
      }
      .lens {
        width: 40px;
        height: 20px;
        border: 3px solid #888;
        border-radius: 4px;
        background: rgba(200,200,200,0.2);
      }
      .bridge {
        width: 12px;
        height: 3px;
        background: #888;
      }
      .temple {
        width: 20px;
        height: 3px;
        background: #888;
      }
      .pencil {
        font-size: 48px;
        animation: swing 0.3s ease-in-out infinite alternate;
        animation-delay: 0.15s;
        transform-origin: top center;
        display: inline-block;
      }
      @keyframes swing {
        0% { transform: rotate(-15deg); }
        100% { transform: rotate(15deg); }
      }
      @keyframes fadeOut {
        to { opacity: 0; visibility: hidden; }
      }
    </style>
    <div class="preloader">
      <div class="glasses">
        <div class="temple"></div>
        <div class="lens"></div>
        <div class="bridge"></div>
        <div class="lens"></div>
        <div class="temple"></div>
      </div>
      <span class="pencil">✏️</span>
    </div>
    
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>PDF図面比較ツール</title>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
        <style>
            * { margin: 0; padding: 0; box-sizing: border-box; }
            body {
                font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
                background: linear-gradient(135deg, #667eea 0%,  #764ba2 100%);
                min-height: 100vh;
                padding: 20px;
            }
            .container {
                max-width: 1400px;
                margin: 0 auto;
                background: white;
                border-radius: 20px;
                box-shadow: 0 20px 60px rgba(0,0,0,0.3);
                padding: 30px;
            }
            
            .header-row {
                display: flex;
                align-items: center;
                justify-content: space-between;
                margin-bottom: 30px;
                flex-wrap: wrap;
                gap: 15px;
            }
            h1 { color: #333; font-size: 1.8em; margin: 0; white-space: nowrap; }
            
            .slot-wrapper {
                display: flex;
                align-items: center;
                gap: 10px;
                background: #f0f0f0;
                padding: 8px 12px;
                border-radius: 6px;
            }
            .slot-label { font-size: 11px; color: #666; font-weight: bold; white-space: nowrap; }
            .slot-container { display: flex; flex-wrap: wrap; gap: 2px; }
            .slot-group { display: flex; gap: 1px; align-items: center; margin-right: 3px; }
            .slot-btn {
                padding: 5px 8px;
                font-size: 10px;
                border: none;
                border-radius: 3px;
                cursor: pointer;
                color: #fff;
                transition: all 0.2s;
            }
            .slot-btn.save { background: #4CAF50; }
            .slot-btn.save:hover { background: #45a049; }
            .slot-btn.load { background: #2196F3; }
            .slot-btn.load:hover { background: #1976D2; }
            .slot-btn.load:disabled { background: #ccc; cursor: not-allowed; }
            .slot-btn.delete { background: #f44336; padding: 5px 6px; }
            .slot-btn.delete:hover { background: #d32f2f; }
            .toast {
                position: fixed;
                bottom: 30px;
                left: 50%;
                transform: translateX(-50%);
                background: #333;
                color: #fff;
                padding: 12px 24px;
                border-radius: 6px;
                z-index: 9999;
                opacity: 0;
                transition: opacity 0.3s;
                pointer-events: none;
            }
            .toast.show { opacity: 1; }
            
            .drop-zones { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 30px; }
            .drop-zone {
                border: 3px dashed #ccc;
                border-radius: 10px;
                padding: 40px 20px;
                text-align: center;
                background: #f9f9f9;
                transition: all 0.3s ease;
                cursor: pointer;
                min-height: 200px;
                display: flex;
                flex-direction: column;
                justify-content: center;
                align-items: center;
            }
            .drop-zone:hover { border-color: #667eea; background: #f0f4ff; }
            .drop-zone.dragover { border-color: #764ba2; background: #ffe4f3; transform: scale(1.02); }
            .drop-zone.loaded { border-color: #4caf50; background: #e8f5e9; }
            .drop-zone h3 { color: #666; margin-bottom: 10px; }
            .drop-zone p { color: #999; font-size: 0.9em; }
            .file-info { margin-top: 10px; padding: 10px; background: white; border-radius: 5px; font-size: 0.9em; color: #333; }
            .page-info { margin-top: 5px; color: #667eea; font-weight: bold; }
            .controls { background: #f5f5f5; padding: 20px; border-radius: 10px; margin-bottom: 20px; }
            .control-group { margin-bottom: 15px; }
            .control-group label { display: block; margin-bottom: 5px; color: #555; font-weight: 500; }
            .page-selector {
                display: grid;
                grid-template-columns: 1fr 1fr;
                gap: 20px;
                padding: 15px;
                background: white;
                border-radius: 8px;
                margin-bottom: 15px;
            }
            .page-input-group { display: flex; align-items: center; gap: 10px; }
            .page-input-group label { margin: 0; min-width: 80px; }
            .page-input { flex: 1; padding: 8px; border: 2px solid #ddd; border-radius: 5px; font-size: 1em; }
            .page-input:focus { border-color: #667eea; outline: none; }
            .page-range { font-size: 0.9em; color: #666; margin-left: 10px; }
            .batch-mode {
                display: flex;
                align-items: center;
                gap: 15px;
                padding: 10px;
                background: #fff3cd;
                border-radius: 5px;
                margin-bottom: 15px;
            }
            .batch-mode input[type="checkbox"] { width: 20px; height: 20px; cursor: pointer; }
            .radio-group { display: flex; gap: 20px; flex-wrap: wrap; }
            .radio-option {
                display: flex;
                align-items: center;
                padding: 8px 12px;
                background: white;
                border-radius: 5px;
                cursor: pointer;
                transition: all 0.3s ease;
            }
            .radio-option:hover { background: #e3f2fd; }
            .radio-option input[type="radio"] { margin-right: 8px; }
            .slider-container { display: flex; align-items: center; gap: 15px; }
            input[type="range"] { flex: 1; height: 6px; background: #ddd; border-radius: 3px; outline: none; }
            input[type="range"]::-webkit-slider-thumb {
                -webkit-appearance: none;
                width: 20px;
                height: 20px;
                background: #667eea;
                border-radius: 50%;
                cursor: pointer;
            }
            .slider-value { min-width: 50px; text-align: center; font-weight: bold; color: #667eea; }
            .compare-button {
                display: block;
                width: 100%;
                padding: 15px;
                background: linear-gradient(135deg, #667eea 0%,  #764ba2 100%);
                color: white;
                border: none;
                border-radius: 10px;
                font-size: 1.1em;
                font-weight: bold;
                cursor: pointer;
                transition: all 0.3s ease;
                margin-bottom: 20px;
            }
            .compare-button:hover:not(:disabled) { transform: translateY(-2px); box-shadow: 0 10px 20px rgba(0,0,0,0.2); }
            .compare-button:disabled { background: #ccc; cursor: not-allowed; }
            .results-tabs { display: none; margin-bottom: 20px; }
            .tabs-header { display: flex; gap: 5px; border-bottom: 2px solid #ddd; overflow-x: auto; padding-bottom: 0; }
            .tab-button {
                padding: 10px 20px;
                background: #f5f5f5;
                border: none;
                border-radius: 10px 10px 0 0;
                cursor: pointer;
                transition: all 0.3s ease;
                white-space: nowrap;
                font-weight: 500;
            }
            .tab-button:hover { background: #e0e0e0; }
            .tab-button.active {
                background: white;
                border: 2px solid #ddd;
                border-bottom: 2px solid white;
                margin-bottom: -2px;
                color: #667eea;
            }
            .zoom-controls {
                position: absolute;
                top: 10px;
                right: 10px;
                display: flex;
                gap: 10px;
                background: white;
                padding: 5px;
                border-radius: 5px;
                box-shadow: 0 2px 10px rgba(0,0,0,0.1);
                z-index: 100;
            }
            .zoom-button {
                width: 35px;
                height: 35px;
                border: none;
                background: #667eea;
                color: white;
                border-radius: 5px;
                cursor: pointer;
                font-size: 18px;
                font-weight: bold;
                transition: all 0.3s ease;
            }
            .zoom-button:hover { background: #764ba2; }
            .zoom-label {
                display: flex;
                align-items: center;
                padding: 0 10px;
                min-width: 60px;
                justify-content: center;
                font-weight: bold;
                color: #667eea;
            }
            .result-container {
                position: relative;
                background: #f9f9f9;
                border-radius: 10px;
                padding: 20px;
                min-height: 600px;
                display: flex;
                justify-content: center;
                align-items: center;
                overflow: hidden;
            }
            .canvas-wrapper {
                position: relative;
                width: 100%;
                height: 600px;
                overflow: hidden;
                border: 1px solid #ddd;
                border-radius: 5px;
                background: white;
            }
            .canvas-container {
                display: none;
                position: absolute;
                cursor: grab;
                transform-origin: center center;
            }
            .canvas-container.active { display: block; }
            .canvas-container.dragging { cursor: grabbing; }
            .result-canvas { display: block; image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges; }
            .loading { display: none; text-align: center; color: #666; }
            .loading.active { display: block; }
            .spinner {
                border: 4px solid #f3f3f3;
                border-top: 4px solid #667eea;
                border-radius: 50%;
                width: 40px;
                height: 40px;
                animation: spin 1s linear infinite;
                margin: 0 auto 10px;
            }
            @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } }
            .legend {
                display: flex;
                justify-content: center;
                gap: 30px;
                margin-bottom: 20px;
                padding: 15px;
                background: #f5f5f5;
                border-radius: 5px;
            }
            .legend-item { display: flex; align-items: center; gap: 8px; }
            .legend-color { width: 20px; height: 20px; border-radius: 3px; border: 1px solid #ddd; }
            .action-buttons { display: none; justify-content: center; gap: 15px; margin-bottom: 20px; }
            .action-buttons.show { display: flex; }
            .action-button {
                padding: 10px 20px;
                background: #4caf50;
                color: white;
                border: none;
                border-radius: 5px;
                cursor: pointer;
                font-weight: bold;
                transition: all 0.3s ease;
            }
            .action-button:hover { background: #45a049; }
            .action-button.secondary { background: #2196F3; }
            .action-button.secondary:hover { background: #1976D2; }
            .action-button.tertiary { background: #ff9800; }
            .action-button.tertiary:hover { background: #f57c00; }
            .progress-bar {
                display: none;
                width: 100%;
                height: 20px;
                background: #f0f0f0;
                border-radius: 10px;
                overflow: hidden;
                margin: 10px 0;
            }
            .progress-bar.active { display: block; }
            .progress-fill {
                height: 100%;
                background: linear-gradient(135deg, #667eea 0%,  #764ba2 100%);
                transition: width 0.3s ease;
                display: flex;
                align-items: center;
                justify-content: center;
                color: white;
                font-size: 0.9em;
            }
            input[type="file"] { display: none; }
            
            .adjust-panel {
                display: none;
                justify-content: center;
                align-items: center;
                gap: 20px;
                padding: 15px;
                background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
                border-radius: 10px;
                margin-top: 20px;
                border: 2px solid #4caf50;
            }
            .adjust-panel.show { display: flex; flex-wrap: wrap; }
            .adjust-panel h4 {
                color: #2e7d32;
                font-size: 14px;
                margin-right: 10px;
            }
            .dpad {
                display: grid;
                grid-template-columns: repeat(3, 40px);
                grid-template-rows: repeat(3, 40px);
                gap: 3px;
            }
            .dpad-btn {
                width: 40px;
                height: 40px;
                border: none;
                background: #4caf50;
                color: white;
                border-radius: 6px;
                cursor: pointer;
                font-size: 18px;
                font-weight: bold;
                transition: all 0.15s;
                display: flex;
                align-items: center;
                justify-content: center;
            }
            .dpad-btn:hover { background: #45a049; transform: scale(1.05); }
            .dpad-btn:active { transform: scale(0.95); background: #388e3c; }
            .dpad-btn.center { background: #ff9800; font-size: 11px; }
            .dpad-btn.center:hover { background: #f57c00; }
            .dpad-btn.empty { visibility: hidden; }
            .adjust-info {
                display: flex;
                gap: 15px;
                font-size: 14px;
                color: #333;
                background: white;
                padding: 10px 15px;
                border-radius: 6px;
            }
            .adjust-info div {
                font-family: monospace;
                font-weight: bold;
            }
            .step-control {
                display: flex;
                align-items: center;
                gap: 8px;
            }
            .step-control label { font-size: 13px; color: #555; }
            .step-control select {
                padding: 8px 12px;
                border: 2px solid #4caf50;
                border-radius: 6px;
                font-size: 14px;
                background: white;
                cursor: pointer;
            }
            
            @media (max-width: 768px) {
                .header-row { flex-direction: column; align-items: flex-start; }
                .drop-zones { grid-template-columns: 1fr; }
                .page-selector { grid-template-columns: 1fr; }
                .radio-group { flex-direction: column; }
            }
        </style>
    </head>
    <body>
        <div class="container">
            <div class="header-row">
                <h1>📐 PDF図面比較ツール</h1>
                <div class="slot-wrapper">
                    <span class="slot-label">保存スロット</span>
                    <div class="slot-container" id="slotContainer"></div>
                </div>
            </div>
            
            <div class="drop-zones">
                <div class="drop-zone" id="dropZone1">
                    <h3>元図面 (PDF)</h3>
                    <p>ファイルをドラッグ&ドロップ<br>またはクリックして選択</p>
                    <div class="file-info" id="fileInfo1" style="display:none;"></div>
                    <input type="file" id="fileInput1" accept=".pdf">
                </div>
                <div class="drop-zone" id="dropZone2">
                    <h3>新図面 (PDF)</h3>
                    <p>ファイルをドラッグ&ドロップ<br>またはクリックして選択</p>
                    <div class="file-info" id="fileInfo2" style="display:none;"></div>
                    <input type="file" id="fileInput2" accept=".pdf">
                </div>
            </div>
            
            <div class="controls">
                <div class="page-selector" id="pageSelector" style="display:none;">
                    <div class="page-input-group">
                        <label>元図面ページ:</label>
                        <input type="text" id="pages1" class="page-input" placeholder="例: 1,3,5-8">
                        <span class="page-range" id="pageRange1"></span>
                    </div>
                    <div class="page-input-group">
                        <label>新図面ページ:</label>
                        <input type="text" id="pages2" class="page-input" placeholder="例: 1,3,5-8">
                        <span class="page-range" id="pageRange2"></span>
                    </div>
                </div>
                
                <div class="batch-mode">
                    <input type="checkbox" id="compareAllPages">
                    <label for="compareAllPages">
                        <strong>全ページを自動比較</strong> (同じページ番号同士を比較)
                    </label>
                </div>
                
                <div class="control-group">
                    <label>表示モード:</label>
                    <div class="radio-group">
                        <label class="radio-option">
                            <input type="radio" name="colorMode" value="diff" checked>
                            <span>差分表示 (緑:削除 / 赤:追加 / 黒:変更なし)</span>
                        </label>
                        <label class="radio-option">
                            <input type="radio" name="colorMode" value="blue_red">
                            <span>青(元) / 赤(新)</span>
                        </label>
                        <label class="radio-option">
                            <input type="radio" name="colorMode" value="gray_red">
                            <span>グレー(元) / 赤(新)</span>
                        </label>
                        <label class="radio-option">
                            <input type="radio" name="colorMode" value="overlay">
                            <span>半透明オーバーレイ</span>
                        </label>
                    </div>
                </div>
                
                <div class="control-group">
                    <label>透明度:</label>
                    <div class="slider-container">
                        <input type="range" id="opacitySlider" min="0" max="100" value="50">
                        <span class="slider-value" id="opacityValue">50%</span>
                    </div>
                </div>
                
                <div class="control-group">
                    <label>線の検出感度:</label>
                    <div class="slider-container">
                        <input type="range" id="thresholdSlider" min="100" max="250" value="180">
                        <span class="slider-value" id="thresholdValue">180</span>
                    </div>
                </div>
            </div>
            
            <button class="compare-button" id="compareButton" disabled>比較を実行</button>
            
            <div class="progress-bar" id="progressBar">
                <div class="progress-fill" id="progressFill" style="width: 0%">0%</div>
            </div>
            
            <div class="loading" id="loading">
                <div class="spinner"></div>
                <p>処理中...</p>
            </div>
            
            <div class="action-buttons" id="actionButtons">
                <button class="action-button" id="downloadCurrentButton">現在のページをPDFダウンロード</button>
                <button class="action-button secondary" id="downloadAllButton">全ページを1つのPDFでダウンロード</button>
                <button class="action-button tertiary" id="downloadAllSeparateButton">全ページを個別PDFでダウンロード</button>
            </div>
            
            <div class="legend" id="legend" style="display:none;">
                <div class="legend-item" id="legendDiff1">
                    <div class="legend-color" style="background:#00C800;"></div>
                    <span>削除された線</span>
                </div>
                <div class="legend-item" id="legendDiff2">
                    <div class="legend-color" style="background:#FF0000;"></div>
                    <span>追加された線</span>
                </div>
                <div class="legend-item" id="legendDiff3">
                    <div class="legend-color" style="background:#000000;"></div>
                    <span>変更なし</span>
                </div>
                <div class="legend-item" id="legendBlue" style="display:none;">
                    <div class="legend-color" style="background:#0000FF;"></div>
                    <span>元図面</span>
                </div>
                <div class="legend-item" id="legendRed" style="display:none;">
                    <div class="legend-color" style="background:#FF0000;"></div>
                    <span>新図面</span>
                </div>
            </div>
            
            <div class="results-tabs" id="resultsTabs">
                <div class="tabs-header" id="tabsHeader"></div>
            </div>
            
            <div class="result-container">
                <div class="zoom-controls" id="zoomControls" style="display:none;">
                    <button class="zoom-button" onclick="zoomIn()">+</button>
                    <span class="zoom-label" id="zoomLabel">100%</span>
                    <button class="zoom-button" onclick="zoomOut()">-</button>
                    <button class="zoom-button" onclick="resetZoom()">⟲</button>
                </div>
                <div class="canvas-wrapper" id="canvasWrapper">
                    <div id="canvasContainers"></div>
                </div>
                <p id="placeholder" style="color:#999; position:absolute;">比較結果がここに表示されます</p>
            </div>
            
            <div class="adjust-panel" id="adjustPanel">
                <h4>🎯 新図面の位置調整</h4>
                <div class="dpad">
                    <button class="dpad-btn empty"></button>
                    <button class="dpad-btn" onclick="adjustPosition(0, -1)">▲</button>
                    <button class="dpad-btn empty"></button>
                    <button class="dpad-btn" onclick="adjustPosition(-1, 0)">◀</button>
                    <button class="dpad-btn center" onclick="resetAdjustment()">RST</button>
                    <button class="dpad-btn" onclick="adjustPosition(1, 0)">▶</button>
                    <button class="dpad-btn empty"></button>
                    <button class="dpad-btn" onclick="adjustPosition(0, 1)">▼</button>
                    <button class="dpad-btn empty"></button>
                </div>
                <div class="adjust-info">
                    <div>X: <span id="offsetX">0</span>px</div>
                    <div>Y: <span id="offsetY">0</span>px</div>
                </div>
                <div class="step-control">
                    <label>移動量:</label>
                    <select id="adjustStep">
                        <option value="1">1px</option>
                        <option value="5" selected>5px</option>
                        <option value="10">10px</option>
                        <option value="20">20px</option>
                        <option value="50">50px</option>
                    </select>
                </div>
            </div>
        </div>
        
        <div class="toast" id="toast"></div>
    
        <script>
            pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
            
            const { jsPDF } = window.jspdf;
            
            const DB_NAME = 'pdfCompareToolDB_v4';
            const DB_VERSION = 1;
            const STORE_NAME = 'compareSlots';
            const SLOT_COUNT = 5;
            
            let db = null;
            let file1 = null;
            let file2 = null;
            let pdf1 = null;
            let pdf2 = null;
            let pdf1Bytes = null;
            let pdf2Bytes = null;
            let totalPages1 = 0;
            let totalPages2 = 0;
            let comparisonResults = [];
            let currentTab = 0;
            
            let offsetX = 0;
            let offsetY = 0;
            
            let currentZoom = 1;
            let minZoom = 0.1;
            let maxZoom = 5;
            let zoomStep = 0.2;
            let isDragging = false;
            let startX = 0;
            let startY = 0;
            let translateX = 0;
            let translateY = 0;
            
            /* ============================================================
             *  凡例をキャンバス上に描画する関数
             * ============================================================ */
            function drawLegendOnCanvas(canvas, colorMode) {
                const ctx = canvas.getContext('2d');
                
                let items = [];
                if (colorMode === 'diff') {
                    items = [
                        { color: '#00C800', label: '削除された線' },
                        { color: '#FF0000', label: '追加された線' },
                        { color: '#000000', label: '変更なし' }
                    ];
                } else if (colorMode === 'blue_red') {
                    items = [
                        { color: '#0000FF', label: '元図面' },
                        { color: '#FF0000', label: '新図面' },
                        { color: '#800080', label: '共通' }
                    ];
                } else if (colorMode === 'gray_red') {
                    items = [
                        { color: '#969696', label: '元図面' },
                        { color: '#FF0000', label: '新図面' }
                    ];
                } else if (colorMode === 'overlay') {
                    items = [
                        { color: '#888888', label: '元図面(半透明)' },
                        { color: '#444444', label: '新図面(半透明)' }
                    ];
                }
                
                if (items.length === 0) return;
                
                // 凡例のサイズ計算
                const scale = Math.max(1, canvas.width / 1200); // キャンバスサイズに応じてスケール
                const fontSize = Math.round(14 * scale);
                const boxSize = Math.round(16 * scale);
                const padding = Math.round(12 * scale);
                const itemGap = Math.round(24 * scale);
                const textGap = Math.round(6 * scale);
                const cornerRadius = Math.round(6 * scale);
                const borderWidth = Math.round(1.5 * scale);
                
                ctx.font = `${fontSize}px "Segoe UI", "Hiragino Kaku Gothic ProN", "Meiryo", sans-serif`;
                
                // 全体幅を計算
                let totalWidth = padding * 2;
                items.forEach((item, idx) => {
                    totalWidth += boxSize + textGap + ctx.measureText(item.label).width;
                    if (idx < items.length - 1) totalWidth += itemGap;
                });
                const legendHeight = boxSize + padding * 2;
                
                // 右下に配置
                const margin = Math.round(10 * scale);
                const legendX = canvas.width - totalWidth - margin;
                const legendY = canvas.height - legendHeight - margin;
                
                // 背景(角丸四角)
                ctx.save();
                ctx.globalAlpha = 0.92;
                ctx.fillStyle = '#FFFFFF';
                ctx.beginPath();
                ctx.moveTo(legendX + cornerRadius, legendY);
                ctx.lineTo(legendX + totalWidth - cornerRadius, legendY);
                ctx.arcTo(legendX + totalWidth, legendY, legendX + totalWidth, legendY + cornerRadius, cornerRadius);
                ctx.lineTo(legendX + totalWidth, legendY + legendHeight - cornerRadius);
                ctx.arcTo(legendX + totalWidth, legendY + legendHeight, legendX + totalWidth - cornerRadius, legendY + legendHeight, cornerRadius);
                ctx.lineTo(legendX + cornerRadius, legendY + legendHeight);
                ctx.arcTo(legendX, legendY + legendHeight, legendX, legendY + legendHeight - cornerRadius, cornerRadius);
                ctx.lineTo(legendX, legendY + cornerRadius);
                ctx.arcTo(legendX, legendY, legendX + cornerRadius, legendY, cornerRadius);
                ctx.closePath();
                ctx.fill();
                ctx.globalAlpha = 1.0;
                
                // 枠線
                ctx.strokeStyle = '#CCCCCC';
                ctx.lineWidth = borderWidth;
                ctx.stroke();
                ctx.restore();
                
                // 各アイテム描画
                let curX = legendX + padding;
                const curY = legendY + padding;
                
                items.forEach((item, idx) => {
                    // 色ボックス
                    ctx.fillStyle = item.color;
                    ctx.fillRect(curX, curY, boxSize, boxSize);
                    ctx.strokeStyle = '#AAAAAA';
                    ctx.lineWidth = Math.max(1, borderWidth * 0.5);
                    ctx.strokeRect(curX, curY, boxSize, boxSize);
                    
                    curX += boxSize + textGap;
                    
                    // テキスト
                    ctx.fillStyle = '#333333';
                    ctx.font = `${fontSize}px "Segoe UI", "Hiragino Kaku Gothic ProN", "Meiryo", sans-serif`;
                    ctx.textBaseline = 'middle';
                    ctx.fillText(item.label, curX, curY + boxSize / 2);
                    
                    curX += ctx.measureText(item.label).width;
                    if (idx < items.length - 1) curX += itemGap;
                });
            }
            
            /* ============================================================
             *  キャンバスをPDFに変換するユーティリティ
             * ============================================================ */
            function canvasToPdfBlob(canvasList) {
                // canvasList: [{canvas, page1, page2}, ...]
                let pdfDoc = null;
                
                canvasList.forEach((item, idx) => {
                    const canvas = item.canvas;
                    const imgData = canvas.toDataURL('image/jpeg', 0.92);
                    
                    // キャンバスのアスペクト比からPDFページサイズを決定
                    const cWidth = canvas.width;
                    const cHeight = canvas.height;
                    const orientation = cWidth >= cHeight ? 'landscape' : 'portrait';
                    
                    if (idx === 0) {
                        pdfDoc = new jsPDF({
                            orientation: orientation,
                            unit: 'pt',
                            format: [cWidth * 0.75, cHeight * 0.75] // 96dpi → 72dpi 変換
                        });
                    } else {
                        pdfDoc.addPage([cWidth * 0.75, cHeight * 0.75], orientation);
                    }
                    
                    const pageW = pdfDoc.internal.pageSize.getWidth();
                    const pageH = pdfDoc.internal.pageSize.getHeight();
                    
                    pdfDoc.addImage(imgData, 'JPEG', 0, 0, pageW, pageH);
                });
                
                return pdfDoc;
            }
            
            function adjustPosition(dx, dy) {
                const step = parseInt(document.getElementById('adjustStep').value);
                offsetX += dx * step;
                offsetY += dy * step;
                updateOffsetDisplay();
                if (comparisonResults.length > 0) {
                    comparePDFs();
                }
            }
            
            function resetAdjustment() {
                offsetX = 0;
                offsetY = 0;
                updateOffsetDisplay();
                if (comparisonResults.length > 0) {
                    comparePDFs();
                }
            }
            
            function updateOffsetDisplay() {
                document.getElementById('offsetX').textContent = offsetX;
                document.getElementById('offsetY').textContent = offsetY;
            }
            
            function initDB() {
                return new Promise((resolve, reject) => {
                    const request = indexedDB.open(DB_NAME, DB_VERSION);
                    request.onerror = () => reject(request.error);
                    request.onsuccess = () => { db = request.result; resolve(db); };
                    request.onupgradeneeded = (e) => {
                        const database = e.target.result;
                        if (!database.objectStoreNames.contains(STORE_NAME)) {
                            database.createObjectStore(STORE_NAME, { keyPath: 'id' });
                        }
                    };
                });
            }
            
            function getSlots() {
                return new Promise((resolve, reject) => {
                    if (!db) { resolve({}); return; }
                    try {
                        const tx = db.transaction(STORE_NAME, 'readonly');
                        const store = tx.objectStore(STORE_NAME);
                        const request = store.getAll();
                        request.onsuccess = () => {
                            const slots = {};
                            request.result.forEach(item => { slots[item.id] = item; });
                            resolve(slots);
                        };
                        request.onerror = () => reject(request.error);
                    } catch (e) { resolve({}); }
                });
            }
            
            function saveSlotToDB(slotNum, data) {
                return new Promise((resolve, reject) => {
                    if (!db) { reject(new Error('DB not initialized')); return; }
                    try {
                        const tx = db.transaction(STORE_NAME, 'readwrite');
                        const store = tx.objectStore(STORE_NAME);
                        data.id = slotNum;
                        const request = store.put(data);
                        request.onsuccess = () => resolve();
                        request.onerror = () => reject(request.error);
                    } catch (e) { reject(e); }
                });
            }
            
            function deleteSlotFromDB(slotNum) {
                return new Promise((resolve, reject) => {
                    if (!db) { reject(new Error('DB not initialized')); return; }
                    try {
                        const tx = db.transaction(STORE_NAME, 'readwrite');
                        const store = tx.objectStore(STORE_NAME);
                        const request = store.delete(slotNum);
                        request.onsuccess = () => resolve();
                        request.onerror = () => reject(request.error);
                    } catch (e) { reject(e); }
                });
            }
            
            async function saveToSlot(slotNum) {
                if (!pdf1Bytes || !pdf2Bytes) {
                    alert('両方のPDFを読み込んでください');
                    return;
                }
                
                try {
                    const data = {
                        pdf1Data: Array.from(pdf1Bytes),
                        pdf2Data: Array.from(pdf2Bytes),
                        file1Name: file1 ? file1.name : 'PDF1',
                        file2Name: file2 ? file2.name : 'PDF2',
                        pages1: document.getElementById('pages1').value,
                        pages2: document.getElementById('pages2').value,
                        compareAllPages: document.getElementById('compareAllPages').checked,
                        colorMode: document.querySelector('input[name="colorMode"]:checked').value,
                        opacity: document.getElementById('opacitySlider').value,
                        threshold: document.getElementById('thresholdSlider').value,
                        offsetX: offsetX,
                        offsetY: offsetY,
                        adjustStep: document.getElementById('adjustStep').value,
                        savedAt: new Date().toLocaleString('ja-JP')
                    };
                    
                    await saveSlotToDB(slotNum, data);
                    showToast(`スロット${slotNum}に保存しました`);
                    await renderSlotUI();
                } catch (e) {
                    console.error('saveToSlot error:', e);
                    alert('保存に失敗しました: ' + e.message);
                }
            }
            
            async function loadFromSlot(slotNum) {
                try {
                    const slots = await getSlots();
                    const slot = slots[slotNum];
                    
                    if (!slot) {
                        alert('データがありません');
                        return;
                    }
                    
                    pdf1Bytes = new Uint8Array(slot.pdf1Data);
                    pdf2Bytes = new Uint8Array(slot.pdf2Data);
                    
                    pdf1 = await pdfjsLib.getDocument({ data: pdf1Bytes.slice() }).promise;
                    totalPages1 = pdf1.numPages;
                    file1 = { name: slot.file1Name, size: pdf1Bytes.length };
                    
                    document.getElementById('fileInfo1').innerHTML = `
                        📄 ${slot.file1Name} (${(pdf1Bytes.length / 1024).toFixed(1)} KB)
                        <div class="page-info">ページ数: ${totalPages1}</div>
                    `;
                    document.getElementById('fileInfo1').style.display = 'block';
                    document.getElementById('dropZone1').classList.add('loaded');
                    document.getElementById('pageRange1').textContent = `(1-${totalPages1})`;
                    
                    pdf2 = await pdfjsLib.getDocument({ data: pdf2Bytes.slice() }).promise;
                    totalPages2 = pdf2.numPages;
                    file2 = { name: slot.file2Name, size: pdf2Bytes.length };
                    
                    document.getElementById('fileInfo2').innerHTML = `
                        📄 ${slot.file2Name} (${(pdf2Bytes.length / 1024).toFixed(1)} KB)
                        <div class="page-info">ページ数: ${totalPages2}</div>
                    `;
                    document.getElementById('fileInfo2').style.display = 'block';
                    document.getElementById('dropZone2').classList.add('loaded');
                    document.getElementById('pageRange2').textContent = `(1-${totalPages2})`;
                    
                    document.getElementById('pages1').value = slot.pages1 || '1';
                    document.getElementById('pages2').value = slot.pages2 || '1';
                    document.getElementById('compareAllPages').checked = slot.compareAllPages || false;
                    
                    const colorRadio = document.querySelector(`input[name="colorMode"][value="${slot.colorMode}"]`);
                    if (colorRadio) colorRadio.checked = true;
                    
                    document.getElementById('opacitySlider').value = slot.opacity || 50;
                    document.getElementById('opacityValue').textContent = (slot.opacity || 50) + '%';
                    document.getElementById('thresholdSlider').value = slot.threshold || 180;
                    document.getElementById('thresholdValue').textContent = slot.threshold || 180;
                    
                    offsetX = slot.offsetX || 0;
                    offsetY = slot.offsetY || 0;
                    updateOffsetDisplay();
                    if (slot.adjustStep) {
                        document.getElementById('adjustStep').value = slot.adjustStep;
                    }
                    
                    const pageInputs = document.querySelectorAll('.page-input');
                    pageInputs.forEach(input => {
                        input.disabled = slot.compareAllPages;
                        input.style.opacity = slot.compareAllPages ? '0.5' : '1';
                    });
                    
                    document.getElementById('compareButton').disabled = false;
                    document.getElementById('pageSelector').style.display = 'grid';
                    document.getElementById('adjustPanel').classList.add('show');
                    
                    showToast(`スロット${slotNum}を読み込みました`);
                } catch (e) {
                    console.error('loadFromSlot error:', e);
                    alert('読み込みに失敗しました: ' + e.message);
                }
            }
            
            async function deleteSlot(slotNum, e) {
                e.stopPropagation();
                if (!confirm(`スロット${slotNum}を削除しますか?`)) return;
                try {
                    await deleteSlotFromDB(slotNum);
                    await renderSlotUI();
                    showToast(`スロット${slotNum}を削除しました`);
                } catch (e) {
                    console.error('deleteSlot error:', e);
                }
            }
            
            async function renderSlotUI() {
                const container = document.getElementById('slotContainer');
                try {
                    const slots = await getSlots();
                    let html = '';
                    for (let i = 1; i <= SLOT_COUNT; i++) {
                        const slot = slots[i];
                        const hasData = !!slot;
                        html += `<div class="slot-group">
                            <button class="slot-btn save" onclick="saveToSlot(${i})" title="${hasData ? '上書き保存\n' + slot.savedAt : '新規保存'}">S${i}</button>
                            <button class="slot-btn load" onclick="loadFromSlot(${i})" ${hasData ? '' : 'disabled'} title="${hasData ? '読込: ' + slot.savedAt : '空'}">L${i}</button>
                            ${hasData ? `<button class="slot-btn delete" onclick="deleteSlot(${i}, event)" title="削除">×</button>` : ''}
                        </div>`;
                    }
                    container.innerHTML = html;
                } catch (e) {
                    console.error('renderSlotUI error:', e);
                }
            }
            
            function showToast(message) {
                const toast = document.getElementById('toast');
                toast.textContent = message;
                toast.classList.add('show');
                setTimeout(() => toast.classList.remove('show'), 2000);
            }
            
            function zoomIn() {
                if (currentZoom < maxZoom) {
                    currentZoom = Math.min(currentZoom + zoomStep, maxZoom);
                    applyZoom();
                }
            }
            
            function zoomOut() {
                if (currentZoom > minZoom) {
                    currentZoom = Math.max(currentZoom - zoomStep, minZoom);
                    applyZoom();
                }
            }
            
            function resetZoom() {
                currentZoom = 1;
                translateX = 0;
                translateY = 0;
                applyZoom();
            }
            
            function applyZoom() {
                const activeContainer = document.querySelector('.canvas-container.active');
                if (activeContainer) {
                    activeContainer.style.transform = `translate(${translateX}px, ${translateY}px) scale(${currentZoom})`;
                    document.getElementById('zoomLabel').textContent = Math.round(currentZoom * 100) + '%';
                }
            }
            
            function setupZoomControls() {
                const canvasWrapper = document.getElementById('canvasWrapper');
                
                canvasWrapper.addEventListener('wheel', (e) => {
                    e.preventDefault();
                    const delta = e.deltaY > 0 ? -0.1 : 0.1;
                    const newZoom = Math.max(minZoom, Math.min(maxZoom, currentZoom + delta));
                    
                    if (newZoom !== currentZoom) {
                        const rect = canvasWrapper.getBoundingClientRect();
                        const x = e.clientX - rect.left;
                        const y = e.clientY - rect.top;
                        const scale = newZoom / currentZoom;
                        translateX = x - scale * (x - translateX);
                        translateY = y - scale * (y - translateY);
                        currentZoom = newZoom;
                        applyZoom();
                    }
                });
                
                let activeContainer = null;
                
                canvasWrapper.addEventListener('mousedown', (e) => {
                    activeContainer = document.querySelector('.canvas-container.active');
                    if (activeContainer && e.button === 0) {
                        isDragging = true;
                        startX = e.clientX - translateX;
                        startY = e.clientY - translateY;
                        activeContainer.classList.add('dragging');
                    }
                });
                
                document.addEventListener('mousemove', (e) => {
                    if (isDragging && activeContainer) {
                        translateX = e.clientX - startX;
                        translateY = e.clientY - startY;
                        applyZoom();
                    }
                });
                
                document.addEventListener('mouseup', () => {
                    if (activeContainer) activeContainer.classList.remove('dragging');
                    isDragging = false;
                    activeContainer = null;
                });
            }
            
            function setupDropZone(dropZone, fileInput, fileInfoDiv, fileNumber) {
                dropZone.addEventListener('click', () => fileInput.click());
                
                fileInput.addEventListener('change', (e) => {
                    handleFile(e.target.files[0], fileNumber, fileInfoDiv, dropZone);
                });
                
                dropZone.addEventListener('dragover', (e) => {
                    e.preventDefault();
                    dropZone.classList.add('dragover');
                });
                
                dropZone.addEventListener('dragleave', () => dropZone.classList.remove('dragover'));
                
                dropZone.addEventListener('drop', (e) => {
                    e.preventDefault();
                    dropZone.classList.remove('dragover');
                    const files = e.dataTransfer.files;
                    if (files.length > 0 && files[0].type === 'application/pdf') {
                        handleFile(files[0], fileNumber, fileInfoDiv, dropZone);
                    }
                });
            }
            
            async function handleFile(file, fileNumber, fileInfoDiv, dropZone) {
                if (!file || file.type !== 'application/pdf') {
                    alert('PDFファイルを選択してください');
                    return;
                }
                
                const arrayBuffer = await file.arrayBuffer();
                const bytes = new Uint8Array(arrayBuffer).slice();
                const pdf = await pdfjsLib.getDocument({ data: bytes.slice() }).promise;
                
                if (fileNumber === 1) {
                    file1 = file;
                    pdf1 = pdf;
                    pdf1Bytes = bytes;
                    totalPages1 = pdf.numPages;
                    fileInfoDiv.innerHTML = `
                        📄 ${file.name} (${(file.size / 1024).toFixed(1)} KB)
                        <div class="page-info">ページ数: ${totalPages1}</div>
                    `;
                    document.getElementById('pageRange1').textContent = `(1-${totalPages1})`;
                } else {
                    file2 = file;
                    pdf2 = pdf;
                    pdf2Bytes = bytes;
                    totalPages2 = pdf.numPages;
                    fileInfoDiv.innerHTML = `
                        📄 ${file.name} (${(file.size / 1024).toFixed(1)} KB)
                        <div class="page-info">ページ数: ${totalPages2}</div>
                    `;
                    document.getElementById('pageRange2').textContent = `(1-${totalPages2})`;
                }
                
                fileInfoDiv.style.display = 'block';
                dropZone.classList.add('loaded');
                checkReady();
            }
            
            function checkReady() {
                const compareButton = document.getElementById('compareButton');
                const pageSelector = document.getElementById('pageSelector');
                const adjustPanel = document.getElementById('adjustPanel');
                
                if (pdf1 && pdf2) {
                    compareButton.disabled = false;
                    pageSelector.style.display = 'grid';
                    adjustPanel.classList.add('show');
                    document.getElementById('pages1').value = '1';
                    document.getElementById('pages2').value = '1';
                }
            }
            
            function parsePageNumbers(input, maxPage) {
                const pages = [];
                const parts = input.split(',');
                
                for (const part of parts) {
                    const trimmed = part.trim();
                    if (trimmed.includes('-')) {
                        const [start, end] = trimmed.split('-').map(n => parseInt(n.trim()));
                        for (let i = start; i <= Math.min(end, maxPage); i++) {
                            if (i >= 1 && i <= maxPage && !pages.includes(i)) pages.push(i);
                        }
                    } else {
                        const page = parseInt(trimmed);
                        if (page >= 1 && page <= maxPage && !pages.includes(page)) pages.push(page);
                    }
                }
                return pages.sort((a, b) => a - b);
            }
            
            async function pdfPageToCanvas(pdf, pageNum, scale = 3.0) {
                const page = await pdf.getPage(pageNum);
                const viewport = page.getViewport({ scale });
                const canvas = document.createElement('canvas');
                const context = canvas.getContext('2d', { alpha: false, willReadFrequently: true });
                
                canvas.width = viewport.width;
                canvas.height = viewport.height;
                context.imageSmoothingEnabled = false;
                
                await page.render({ canvasContext: context, viewport: viewport, intent: 'display' }).promise;
                return canvas;
            }
            
            function updateProgress(current, total) {
                const progressBar = document.getElementById('progressBar');
                const progressFill = document.getElementById('progressFill');
                const percent = Math.round((current / total) * 100);
                progressBar.classList.add('active');
                progressFill.style.width = `${percent}%`;
                progressFill.textContent = `${percent}%`;
            }
            
            async function comparePDFs() {
                const loading = document.getElementById('loading');
                const placeholder = document.getElementById('placeholder');
                const legend = document.getElementById('legend');
                const actionButtons = document.getElementById('actionButtons');
                const resultsTabs = document.getElementById('resultsTabs');
                const progressBar = document.getElementById('progressBar');
                const zoomControls = document.getElementById('zoomControls');
                
                loading.classList.add('active');
                placeholder.style.display = 'none';
                comparisonResults = [];
                
                const colorMode = document.querySelector('input[name="colorMode"]:checked').value;
                
                try {
                    let pagePairs = [];
                    
                    if (document.getElementById('compareAllPages').checked) {
                        const maxPages = Math.min(totalPages1, totalPages2);
                        for (let i = 1; i <= maxPages; i++) {
                            pagePairs.push({ page1: i, page2: i });
                        }
                    } else {
                        const pages1Input = document.getElementById('pages1').value || '1';
                        const pages2Input = document.getElementById('pages2').value || '1';
                        const pages1 = parsePageNumbers(pages1Input, totalPages1);
                        const pages2 = parsePageNumbers(pages2Input, totalPages2);
                        
                        if (pages1.length === 0 || pages2.length === 0) {
                            alert('有効なページ番号を入力してください');
                            loading.classList.remove('active');
                            return;
                        }
                        
                        const maxLength = Math.max(pages1.length, pages2.length);
                        for (let i = 0; i < maxLength; i++) {
                            const page1 = pages1[i % pages1.length];
                            const page2 = pages2[i % pages2.length];
                            pagePairs.push({ page1, page2 });
                        }
                    }
                    
                    const totalPairs = pagePairs.length;
                    for (let i = 0; i < totalPairs; i++) {
                        updateProgress(i + 1, totalPairs);
                        const { page1, page2 } = pagePairs[i];
                        const canvas1 = await pdfPageToCanvas(pdf1, page1);
                        const canvas2 = await pdfPageToCanvas(pdf2, page2);
                        const resultCanvas = compareCanvases(canvas1, canvas2);
                        
                        // 凡例をキャンバスに描き込む
                        drawLegendOnCanvas(resultCanvas, colorMode);
                        
                        comparisonResults.push({ canvas: resultCanvas, page1, page2 });
                    }
                    
                    displayResults();
                    legend.style.display = 'flex';
                    updateLegend();
                    actionButtons.classList.add('show');
                    resultsTabs.style.display = 'block';
                    progressBar.classList.remove('active');
                    zoomControls.style.display = 'flex';
                    resetZoom();
                    
                } catch (error) {
                    console.error('エラー:', error);
                    alert('処理中にエラーが発生しました: ' + error.message);
                } finally {
                    loading.classList.remove('active');
                }
            }
            
            function updateLegend() {
                const colorMode = document.querySelector('input[name="colorMode"]:checked').value;
                
                document.getElementById('legendDiff1').style.display = 'none';
                document.getElementById('legendDiff2').style.display = 'none';
                document.getElementById('legendDiff3').style.display = 'none';
                document.getElementById('legendBlue').style.display = 'none';
                document.getElementById('legendRed').style.display = 'none';
                
                if (colorMode === 'diff') {
                    document.getElementById('legendDiff1').style.display = 'flex';
                    document.getElementById('legendDiff2').style.display = 'flex';
                    document.getElementById('legendDiff3').style.display = 'flex';
                } else {
                    document.getElementById('legendBlue').style.display = 'flex';
                    document.getElementById('legendRed').style.display = 'flex';
                }
            }
            
            function compareCanvases(canvas1, canvas2) {
                const width = Math.max(canvas1.width, canvas2.width + Math.abs(offsetX));
                const height = Math.max(canvas1.height, canvas2.height + Math.abs(offsetY));
                
                const resultCanvas = document.createElement('canvas');
                resultCanvas.width = width;
                resultCanvas.height = height;
                resultCanvas.className = 'result-canvas';
                
                const ctx = resultCanvas.getContext('2d', { alpha: false, willReadFrequently: true });
                ctx.imageSmoothingEnabled = false;
                
                const tempCanvas2 = document.createElement('canvas');
                tempCanvas2.width = width;
                tempCanvas2.height = height;
                const tempCtx2 = tempCanvas2.getContext('2d', { willReadFrequently: true });
                tempCtx2.fillStyle = 'white';
                tempCtx2.fillRect(0, 0, width, height);
                tempCtx2.drawImage(canvas2, offsetX, offsetY);
                
                const tempCanvas1 = document.createElement('canvas');
                tempCanvas1.width = width;
                tempCanvas1.height = height;
                const tempCtx1 = tempCanvas1.getContext('2d', { willReadFrequently: true });
                tempCtx1.fillStyle = 'white';
                tempCtx1.fillRect(0, 0, width, height);
                tempCtx1.drawImage(canvas1, 0, 0);
                
                const imageData1 = tempCtx1.getImageData(0, 0, width, height);
                const imageData2 = tempCtx2.getImageData(0, 0, width, height);
                
                const colorMode = document.querySelector('input[name="colorMode"]:checked').value;
                const opacity = document.getElementById('opacitySlider').value / 100;
                const threshold = parseInt(document.getElementById('thresholdSlider').value);
                
                const resultImageData = ctx.createImageData(width, height);
                
                for (let i = 0; i < resultImageData.data.length; i += 4) {
                    resultImageData.data[i] = 255;
                    resultImageData.data[i + 1] = 255;
                    resultImageData.data[i + 2] = 255;
                    resultImageData.data[i + 3] = 255;
                }
                
                if (colorMode === 'diff') {
                    processDiff(imageData1, imageData2, resultImageData, threshold);
                } else if (colorMode === 'blue_red') {
                    processBlueRed(imageData1, imageData2, resultImageData, threshold, opacity);
                } else if (colorMode === 'gray_red') {
                    processGrayRed(imageData1, imageData2, resultImageData, threshold, opacity);
                } else if (colorMode === 'overlay') {
                    processOverlay(tempCanvas1, tempCanvas2, ctx, opacity);
                    return resultCanvas;
                }
                
                ctx.putImageData(resultImageData, 0, 0);
                return resultCanvas;
            }
            
            function processDiff(data1, data2, result, threshold) {
                for (let i = 0; i < result.data.length; i += 4) {
                    const gray1 = (data1.data[i] + data1.data[i + 1] + data1.data[i + 2]) / 3;
                    const gray2 = (data2.data[i] + data2.data[i + 1] + data2.data[i + 2]) / 3;
                    
                    const isLine1 = gray1 < threshold;
                    const isLine2 = gray2 < threshold;
                    
                    if (isLine1 && !isLine2) {
                        result.data[i] = 0; result.data[i + 1] = 200; result.data[i + 2] = 0;
                    } else if (!isLine1 && isLine2) {
                        result.data[i] = 255; result.data[i + 1] = 0; result.data[i + 2] = 0;
                    } else if (isLine1 && isLine2) {
                        result.data[i] = 0; result.data[i + 1] = 0; result.data[i + 2] = 0;
                    }
                }
            }
            
            function processBlueRed(data1, data2, result, threshold, opacity) {
                for (let i = 0; i < result.data.length; i += 4) {
                    const gray1 = (data1.data[i] + data1.data[i + 1] + data1.data[i + 2]) / 3;
                    const gray2 = (data2.data[i] + data2.data[i + 1] + data2.data[i + 2]) / 3;
                    
                    const isLine1 = gray1 < threshold;
                    const isLine2 = gray2 < threshold;
                    
                    if (isLine1 && isLine2) {
                        result.data[i] = 128; result.data[i + 1] = 0; result.data[i + 2] = 128;
                    } else if (isLine1) {
                        result.data[i] = 0; result.data[i + 1] = 0; result.data[i + 2] = 255;
                    } else if (isLine2) {
                        result.data[i] = 255; result.data[i + 1] = 0; result.data[i + 2] = 0;
                    }
                }
            }
            
            function processGrayRed(data1, data2, result, threshold, opacity) {
                for (let i = 0; i < result.data.length; i += 4) {
                    const gray1 = (data1.data[i] + data1.data[i + 1] + data1.data[i + 2]) / 3;
                    const gray2 = (data2.data[i] + data2.data[i + 1] + data2.data[i + 2]) / 3;
                    
                    const isLine1 = gray1 < threshold;
                    const isLine2 = gray2 < threshold;
                    
                    if (isLine2) {
                        result.data[i] = 255; result.data[i + 1] = 0; result.data[i + 2] = 0;
                    } else if (isLine1) {
                        result.data[i] = 150; result.data[i + 1] = 150; result.data[i + 2] = 150;
                    }
                }
            }
            
            function processOverlay(canvas1, canvas2, ctx, opacity) {
                ctx.fillStyle = 'white';
                ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                ctx.globalAlpha = 1 - opacity;
                ctx.drawImage(canvas1, 0, 0);
                ctx.globalAlpha = opacity;
                ctx.globalCompositeOperation = 'multiply';
                ctx.drawImage(canvas2, 0, 0);
                ctx.globalAlpha = 1;
                ctx.globalCompositeOperation = 'source-over';
            }
            
            function displayResults() {
                const tabsHeader = document.getElementById('tabsHeader');
                const canvasContainers = document.getElementById('canvasContainers');
                
                tabsHeader.innerHTML = '';
                canvasContainers.innerHTML = '';
                
                comparisonResults.forEach((result, index) => {
                    const tabButton = document.createElement('button');
                    tabButton.className = 'tab-button';
                    tabButton.textContent = `P${result.page1} vs P${result.page2}`;
                    tabButton.onclick = () => switchTab(index);
                    if (index === 0) tabButton.classList.add('active');
                    tabsHeader.appendChild(tabButton);
                    
                    const container = document.createElement('div');
                    container.className = 'canvas-container';
                    container.id = `canvas-container-${index}`;
                    if (index === 0) container.classList.add('active');
                    
                    const canvas = result.canvas;
                    const maxWidth = window.innerWidth * 0.8;
                    const maxHeight = 600;
                    
                    let displayWidth = canvas.width;
                    let displayHeight = canvas.height;
                    
                    if (displayWidth > maxWidth) {
                        const ratio = maxWidth / displayWidth;
                        displayWidth = maxWidth;
                        displayHeight = displayHeight * ratio;
                    }
                    
                    if (displayHeight > maxHeight) {
                        const ratio = maxHeight / displayHeight;
                        displayHeight = maxHeight;
                        displayWidth = displayWidth * ratio;
                    }
                    
                    canvas.style.width = displayWidth + 'px';
                    canvas.style.height = displayHeight + 'px';
                    
                    container.appendChild(canvas);
                    container.style.width = displayWidth + 'px';
                    container.style.height = displayHeight + 'px';
                    container.style.left = '50%';
                    container.style.top = '50%';
                    container.style.marginLeft = -displayWidth/2 + 'px';
                    container.style.marginTop = -displayHeight/2 + 'px';
                    
                    canvasContainers.appendChild(container);
                });
                
                currentTab = 0;
            }
            
            function switchTab(index) {
                const tabButtons = document.querySelectorAll('.tab-button');
                const canvasContainers = document.querySelectorAll('.canvas-container');
                
                tabButtons.forEach((btn, i) => btn.classList.toggle('active', i === index));
                canvasContainers.forEach((container, i) => container.classList.toggle('active', i === index));
                
                currentTab = index;
                resetZoom();
            }
            
            /* ============================================================
             *  ダウンロード関数 — すべてPDF出力
             * ============================================================ */
            function downloadCurrent() {
                if (comparisonResults.length === 0) return;
                const result = comparisonResults[currentTab];
                const pdfDoc = canvasToPdfBlob([result]);
                pdfDoc.save(`comparison_P${result.page1}_vs_P${result.page2}.pdf`);
            }
            
            function downloadAll() {
                if (comparisonResults.length === 0) return;
                const pdfDoc = canvasToPdfBlob(comparisonResults);
                pdfDoc.save('comparison_all_pages.pdf');
            }
            
            async function downloadAllSeparate() {
                if (comparisonResults.length === 0) return;
                for (let i = 0; i < comparisonResults.length; i++) {
                    const result = comparisonResults[i];
                    const pdfDoc = canvasToPdfBlob([result]);
                    await new Promise(resolve => setTimeout(resolve, 300));
                    pdfDoc.save(`comparison_P${result.page1}_vs_P${result.page2}.pdf`);
                }
            }
            
            document.addEventListener('keydown', (e) => {
                if (!pdf1 || !pdf2) return;
                if (e.target.tagName === 'INPUT' || e.target.tagName === 'SELECT') return;
                
                const step = parseInt(document.getElementById('adjustStep').value);
                let moved = false;
                
                switch(e.key) {
                    case 'ArrowUp':
                        offsetY -= step;
                        moved = true;
                        break;
                    case 'ArrowDown':
                        offsetY += step;
                        moved = true;
                        break;
                    case 'ArrowLeft':
                        offsetX -= step;
                        moved = true;
                        break;
                    case 'ArrowRight':
                        offsetX += step;
                        moved = true;
                        break;
                }
                
                if (moved) {
                    e.preventDefault();
                    updateOffsetDisplay();
                    if (comparisonResults.length > 0) {
                        comparePDFs();
                    }
                }
            });
            
            setupDropZone(
                document.getElementById('dropZone1'),
                document.getElementById('fileInput1'),
                document.getElementById('fileInfo1'),
                1
            );
            
            setupDropZone(
                document.getElementById('dropZone2'),
                document.getElementById('fileInput2'),
                document.getElementById('fileInfo2'),
                2
            );
            
            document.getElementById('compareButton').addEventListener('click', comparePDFs);
            document.getElementById('downloadCurrentButton').addEventListener('click', downloadCurrent);
            document.getElementById('downloadAllButton').addEventListener('click', downloadAll);
            document.getElementById('downloadAllSeparateButton').addEventListener('click', downloadAllSeparate);
            
            setupZoomControls();
            
            document.getElementById('compareAllPages').addEventListener('change', (e) => {
                const pageInputs = document.querySelectorAll('.page-input');
                pageInputs.forEach(input => {
                    input.disabled = e.target.checked;
                    input.style.opacity = e.target.checked ? '0.5' : '1';
                });
            });
            
            document.querySelectorAll('input[name="colorMode"]').forEach(radio => {
                radio.addEventListener('change', () => {
                    if (comparisonResults.length > 0) updateLegend();
                });
            });
            
            document.getElementById('opacitySlider').addEventListener('input', (e) => {
                document.getElementById('opacityValue').textContent = e.target.value + '%';
            });
            
            document.getElementById('thresholdSlider').addEventListener('input', (e) => {
                document.getElementById('thresholdValue').textContent = e.target.value;
            });
            
            initDB().then(() => renderSlotUI()).catch(e => console.error('DB init failed:', e));
        </script>
    </body>
    </html>
    

    ⑤ PDF重ね図(オーバーレイ)

    <!DOCTYPE html>
    <html lang="ja">
    <head>
    <style>
      .preloader {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: #fff;
        display: flex;
        justify-content: center;
        align-items: center;
        gap: 20px;
        z-index: 9999;
        animation: fadeOut 0.1s 0.3s forwards;
      }
      .glasses {
        display: flex;
        align-items: center;
        animation: swing 0.3s ease-in-out infinite alternate;
        transform-origin: top center;
      }
      .lens {
        width: 40px;
        height: 20px;
        border: 3px solid #888;
        border-radius: 4px;
        background: rgba(200,200,200,0.2);
      }
      .bridge {
        width: 12px;
        height: 3px;
        background: #888;
      }
      .temple {
        width: 20px;
        height: 3px;
        background: #888;
      }
      .pencil {
        font-size: 48px;
        animation: swing 0.3s ease-in-out infinite alternate;
        animation-delay: 0.15s;
        transform-origin: top center;
        display: inline-block;
      }
      @keyframes swing {
        0% { transform: rotate(-15deg); }
        100% { transform: rotate(15deg); }
      }
      @keyframes fadeOut {
        to { opacity: 0; visibility: hidden; }
      }
    </style>
    <div class="preloader">
      <div class="glasses">
        <div class="temple"></div>
        <div class="lens"></div>
        <div class="bridge"></div>
        <div class="lens"></div>
        <div class="temple"></div>
      </div>
      <span class="pencil">✏️</span>
    </div>
    
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PDF重ね図</title>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
    <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: 'Arial', sans-serif; background: #f0f0f0; }
    .container { display: flex; height: 100vh; }
    .sidebar { width: 300px; background: #2c3e50; color: white; padding: 20px; overflow-y: auto; }
    .title-row { display: flex; align-items: flex-end; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; }
    .title-row h2 { margin: 0; }
    .slot-wrapper { display: flex; flex-direction: column; align-items: flex-start; }
    .slot-label { font-size: 9px; color: #95a5a6; margin-bottom: 2px; }
    .slot-container { display: flex; gap: 2px; align-items: center; }
    .slot-group { position: relative; display: flex; gap: 1px; }
    .slot-btn { padding: 4px 6px; font-size: 10px; border-radius: 3px; width: auto; margin: 0; }
    .slot-btn.save { background: #27ae60; }
    .slot-btn.save:hover { background: #229954; }
    .slot-btn.load { background: #3498db; }
    .slot-btn.load:hover { background: #2980b9; }
    .slot-btn.load:disabled { background: #555; cursor: not-allowed; }
    .slot-btn.delete { position: absolute; top: -5px; right: -5px; background: #c0392b; padding: 1px 4px; font-size: 8px; border-radius: 50%; min-width: auto; line-height: 1; }
    .slot-btn.delete:hover { background: #a93226; }
    .control-group { margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #34495e; }
    .control-group h3 { margin-bottom: 10px; font-size: 14px; }
    .file-input { width: 100%; padding: 10px; margin-bottom: 10px; background: #34495e; border: 1px solid #4a5f7a; color: white; cursor: pointer; }
    .slider-container { display: flex; align-items: center; margin: 10px 0; }
    .slider { flex: 1; margin: 0 10px; }
    .scale-input { width: 60px; padding: 5px; background: #34495e; border: 1px solid #4a5f7a; color: white; text-align: center; margin-left: 5px; }
    .scale-input:focus { outline: none; border-color: #3498db; }
    .scale-unit { margin-left: 5px; color: #95a5a6; }
    button { width: 100%; padding: 10px; margin: 5px 0; background: #3498db; color: white; border: none; cursor: pointer; transition: background 0.3s; }
    button:hover { background: #2980b9; }
    button.active { background: #e74c3c; }
    .export-btn { background: #27ae60; }
    .export-btn:hover { background: #229954; }
    .main-area { flex: 1; position: relative; overflow: auto; background: #ecf0f1; }
    #canvas-container { position: relative; margin: 20px; display: inline-block; background: white; background-image: linear-gradient(45deg, #e0e0e0 25%, transparent 25%), linear-gradient(-45deg, #e0e0e0 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #e0e0e0 75%), linear-gradient(-45deg, transparent 75%, #e0e0e0 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px; }
    .pdf-canvas { position: absolute; top: 0; left: 0; border: 1px solid #bdc3c7; }
    #base-canvas { z-index: 1; position: relative; }
    #overlay-wrapper { position: absolute; z-index: 2; }
    #overlay-canvas { cursor: move; display: block; image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges; }
    .resize-handle { position: absolute; width: 10px; height: 10px; background: #3498db; border: 2px solid white; display: none; z-index: 10; }
    #overlay-wrapper:hover .resize-handle { display: block; }
    .resize-handle:hover { background: #e74c3c; }
    .resize-handle.nw { top: -5px; left: -5px; cursor: nw-resize; }
    .resize-handle.ne { top: -5px; right: -5px; cursor: ne-resize; }
    .resize-handle.sw { bottom: -5px; left: -5px; cursor: sw-resize; }
    .resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
    #crop-overlay { z-index: 3; pointer-events: none; position: absolute; top: 0; left: 0; }
    .crop-active #overlay-canvas { cursor: crosshair !important; }
    .scale-match-active #base-canvas { cursor: crosshair !important; }
    .scale-match-active #overlay-canvas { cursor: crosshair !important; }
    .scale-match-info { padding: 10px; background: #34495e; border-radius: 5px; margin-top: 10px; font-size: 12px; display: none; }
    .scale-match-info.active { display: block; }
    .scale-match-status { padding: 5px; margin-top: 5px; background: #2c3e50; border-radius: 3px; font-size: 11px; }
    .point-marker { position: absolute; width: 10px; height: 10px; border-radius: 50%; z-index: 10; pointer-events: none; }
    .point-marker.base-1 { background: #3498db; border: 2px solid white; }
    .point-marker.base-2 { background: #3498db; border: 2px solid white; }
    .point-marker.overlay-1 { background: #e74c3c; border: 2px solid white; }
    .point-marker.overlay-2 { background: #e74c3c; border: 2px solid white; }
    .color-controls { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; }
    .color-input { width: 50px; height: 30px; border: none; cursor: pointer; }
    .transparent-btn { padding: 5px 10px; margin-left: 5px; background: #7f8c8d; font-size: 12px; width: auto; }
    .transparent-btn.active { background: #e74c3c; }
    .page-controls { display: flex; gap: 10px; align-items: center; justify-content: center; }
    .page-info { padding: 0 10px; }
    .preset-colors { display: flex; gap: 5px; margin-top: 10px; }
    .preset-color { width: 30px; height: 30px; border: 2px solid #34495e; cursor: pointer; }
    .preset-color:hover { border-color: white; }
    .mono-options { margin-top: 10px; padding: 10px; background: #34495e; border-radius: 5px; }
    .threshold-container { margin: 10px 0; }
    .color-row { display: flex; align-items: center; margin-bottom: 10px; }
    .color-label { width: 40px; }
    .transparent-indicator { width: 50px; height: 30px; border: 1px solid #fff; background-image: linear-gradient(45deg, #666 25%, transparent 25%), linear-gradient(-45deg, #666 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #666 75%), linear-gradient(-45deg, transparent 75%, #666 75%); background-size: 10px 10px; background-position: 0 0, 0 5px, 5px -5px, -5px 0px; display: none; margin-left: 10px; }
    .crop-info { padding: 10px; background: #34495e; border-radius: 5px; margin-top: 10px; font-size: 12px; }
    .quality-info { padding: 10px; background: #34495e; border-radius: 5px; margin-top: 10px; font-size: 11px; color: #95a5a6; }
    .pdf-size-select { width: 100%; padding: 5px; margin: 10px 0; background: #34495e; border: 1px solid #4a5f7a; color: white; }
    .loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); display: flex; justify-content: center; align-items: center; z-index: 9999; color: white; font-size: 18px; }
    .loading-overlay.hidden { display: none; }
    .toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #27ae60; color: white; padding: 12px 24px; border-radius: 6px; font-size: 14px; z-index: 10000; opacity: 0; transition: opacity 0.3s; pointer-events: none; }
    .toast.show { opacity: 1; }
    </style>
    </head>
    <body>
    
    <div class="loading-overlay hidden" id="loading-overlay"><div>読み込み中...</div></div>
    <div class="toast" id="toast"></div>
    
    <div class="container">
      <div class="sidebar">
        <div class="title-row">
          <h2>PDF重ね図</h2>
          <div class="slot-wrapper">
            <span class="slot-label">保存スロット</span>
            <div class="slot-container" id="slotContainer"></div>
          </div>
        </div>
        
        <div class="control-group">
          <h3>ベースPDF</h3>
          <input type="file" id="base-file" class="file-input" accept=".pdf">
          <div class="page-controls">
            <button id="base-prev">◀</button>
            <span class="page-info" id="base-page">1/1</span>
            <button id="base-next">▶</button>
          </div>
        </div>
        
        <div class="control-group">
          <h3>重ねるPDF</h3>
          <input type="file" id="overlay-file" class="file-input" accept=".pdf">
          <div class="page-controls">
            <button id="overlay-prev">◀</button>
            <span class="page-info" id="overlay-page">1/1</span>
            <button id="overlay-next">▶</button>
          </div>
        </div>
        
        <div class="control-group">
          <h3>縮尺合わせ</h3>
          <button id="scale-match-mode">対角2点で縮尺合わせ</button>
          <button id="scale-match-reset">ポイントリセット</button>
          <div class="scale-match-info" id="scale-match-info">
            <div class="scale-match-status" id="scale-match-status">ベース図面の1点目を選択してください</div>
          </div>
        </div>
        
        <div class="control-group">
          <h3>トリミング</h3>
          <button id="crop-mode">トリミングモード</button>
          <div class="crop-info" id="crop-info" style="display: none;"><div>範囲選択して適用を押してください</div></div>
          <button id="apply-crop">トリミング適用</button>
          <button id="reset-crop">トリミングリセット</button>
        </div>
        
        <div class="control-group">
          <h3>透過度</h3>
          <div class="slider-container">
            <input type="range" id="opacity" class="slider" min="0" max="100" value="50">
            <input type="number" id="opacity-input" class="scale-input" min="0" max="100" value="50">
            <span class="scale-unit">%</span>
          </div>
        </div>
        
        <div class="control-group">
          <h3>色変更</h3>
          <label style="display: block; margin-bottom: 10px;">
            <input type="checkbox" id="color-mode"> 単色モード
          </label>
          
          <div id="normal-color-controls">
            <div class="color-controls">
              <label>色調整:</label>
              <input type="color" id="color-filter" class="color-input" value="#ff0000">
            </div>
          </div>
          
          <div id="mono-color-controls" class="mono-options" style="display: none;">
            <div class="color-row">
              <span class="color-label">黒→</span>
              <input type="color" id="black-replace" class="color-input" value="#000000">
              <button id="black-transparent" class="transparent-btn">透明</button>
              <div id="black-transparent-indicator" class="transparent-indicator"></div>
            </div>
            <div class="color-row">
              <span class="color-label">白→</span>
              <input type="color" id="white-replace" class="color-input" value="#ffffff">
              <button id="white-transparent" class="transparent-btn">透明</button>
              <div id="white-transparent-indicator" class="transparent-indicator"></div>
            </div>
            <div class="threshold-container">
              <label>閾値:</label>
              <div class="slider-container">
                <input type="range" id="threshold" class="slider" min="0" max="255" value="128">
                <input type="number" id="threshold-input" class="scale-input" min="0" max="255" value="128">
              </div>
            </div>
            <label style="display: block; margin-top: 10px;">
              <input type="checkbox" id="invert-mono"> 白黒反転
            </label>
          </div>
          
          <div class="preset-colors">
            <div class="preset-color" style="background: #ff0000" data-color="#ff0000"></div>
            <div class="preset-color" style="background: #00ff00" data-color="#00ff00"></div>
            <div class="preset-color" style="background: #0000ff" data-color="#0000ff"></div>
            <div class="preset-color" style="background: #ffff00" data-color="#ffff00"></div>
            <div class="preset-color" style="background: #ff00ff" data-color="#ff00ff"></div>
            <div class="preset-color" style="background: #00ffff" data-color="#00ffff"></div>
          </div>
          
          <button id="apply-color">色を適用</button>
          <button id="reset-color">色をリセット</button>
        </div>
        
        <div class="control-group">
          <h3>拡大縮小</h3>
          <div class="slider-container">
            <input type="range" id="scale" class="slider" min="50" max="500" value="100">
            <input type="number" id="scale-input" class="scale-input" min="50" max="500" value="100">
            <span class="scale-unit">%</span>
          </div>
          <div class="slider-container">
            <label style="width: 80px;">重ねる側:</label>
            <input type="range" id="overlay-scale" class="slider" min="10" max="500" value="100">
            <input type="number" id="overlay-scale-input" class="scale-input" min="10" max="500" value="100">
            <span class="scale-unit">%</span>
          </div>
        </div>
        
        <div class="control-group">
          <h3>位置リセット</h3>
          <button id="reset-position">中央に配置</button>
          <button id="fit-to-base">ベースに合わせる</button>
        </div>
        
        <div class="control-group">
          <h3>ブレンドモード</h3>
          <select id="blend-mode" style="width: 100%; padding: 5px;">
            <option value="normal">通常</option>
            <option value="multiply">乗算</option>
            <option value="screen">スクリーン</option>
            <option value="overlay">オーバーレイ</option>
            <option value="difference">差分</option>
            <option value="exclusion">除外</option>
            <option value="darken">暗く</option>
            <option value="lighten">明るく</option>
          </select>
        </div>
        
        <div class="control-group">
          <h3>エクスポート</h3>
          <select id="pdf-size" class="pdf-size-select">
            <option value="a3" selected>A3サイズ</option>
            <option value="a4">A4サイズ</option>
            <option value="a2">A2サイズ</option>
            <option value="a1">A1サイズ</option>
          </select>
          <button id="export-jpg" class="export-btn">JPGで保存</button>
          <button id="export-pdf" class="export-btn">PDFで保存</button>
          <button id="export-png" class="export-btn">PNGで保存</button>
          <div class="quality-info">※最高画質出力(A3・300dpi相当)</div>
        </div>
      </div>
      
      <div class="main-area">
        <div id="canvas-container">
          <canvas id="base-canvas" class="pdf-canvas"></canvas>
          <div id="overlay-wrapper">
            <canvas id="overlay-canvas"></canvas>
            <div class="resize-handle nw"></div>
            <div class="resize-handle ne"></div>
            <div class="resize-handle sw"></div>
            <div class="resize-handle se"></div>
          </div>
          <canvas id="crop-overlay" class="pdf-canvas"></canvas>
        </div>
      </div>
    </div>
    
    <script>
    pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
    
    function showToast(message, duration = 2000) {
      const toast = document.getElementById('toast');
      toast.textContent = message;
      toast.classList.add('show');
      setTimeout(() => toast.classList.remove('show'), duration);
    }
    
    const DB_NAME = 'pdfOverlayDB';
    const DB_VERSION = 1;
    const STORE_NAME = 'slots';
    let db = null;
    
    async function initDB() {
      return new Promise((resolve, reject) => {
        const request = indexedDB.open(DB_NAME, DB_VERSION);
        request.onerror = () => reject(request.error);
        request.onsuccess = () => { db = request.result; resolve(db); };
        request.onupgradeneeded = (e) => {
          const database = e.target.result;
          if (!database.objectStoreNames.contains(STORE_NAME)) database.createObjectStore(STORE_NAME, { keyPath: 'id' });
        };
      });
    }
    
    async function saveSlotToDB(slotNum, data) {
      return new Promise((resolve, reject) => {
        const tx = db.transaction(STORE_NAME, 'readwrite');
        const store = tx.objectStore(STORE_NAME);
        data.id = slotNum;
        store.put(data);
        tx.oncomplete = () => resolve();
        tx.onerror = () => reject(tx.error);
      });
    }
    
    async function getSlotFromDB(slotNum) {
      return new Promise((resolve, reject) => {
        const tx = db.transaction(STORE_NAME, 'readonly');
        const store = tx.objectStore(STORE_NAME);
        const request = store.get(slotNum);
        request.onsuccess = () => resolve(request.result || null);
        request.onerror = () => reject(request.error);
      });
    }
    
    async function deleteSlotFromDB(slotNum) {
      return new Promise((resolve, reject) => {
        const tx = db.transaction(STORE_NAME, 'readwrite');
        const store = tx.objectStore(STORE_NAME);
        store.delete(slotNum);
        tx.oncomplete = () => resolve();
        tx.onerror = () => reject(tx.error);
      });
    }
    
    async function getAllSlots() {
      return new Promise((resolve, reject) => {
        const tx = db.transaction(STORE_NAME, 'readonly');
        const store = tx.objectStore(STORE_NAME);
        const request = store.getAll();
        request.onsuccess = () => {
          const result = {};
          request.result.forEach(slot => { result[slot.id] = slot; });
          resolve(result);
        };
        request.onerror = () => reject(request.error);
      });
    }
    
    const SLOT_COUNT = 5;
    let basePdfArrayBuffer = null;
    let overlayPdfArrayBuffer = null;
    
    function showLoading() { document.getElementById('loading-overlay').classList.remove('hidden'); }
    function hideLoading() { document.getElementById('loading-overlay').classList.add('hidden'); }
    
    async function saveToSlot(slotNum) {
      showLoading();
      try {
        const data = {
          id: slotNum,
          basePdf: basePdfArrayBuffer,
          overlayPdf: overlayPdfArrayBuffer,
          baseCurrentPage: baseCurrentPage,
          overlayCurrentPage: overlayCurrentPage,
          currentScale: currentScale,
          overlayScale: overlayScale,
          overlayX: overlayX,
          overlayY: overlayY,
          relativePosition: relativePosition,
          isPositionSet: isPositionSet,
          opacity: document.getElementById('opacity').value,
          blendMode: document.getElementById('blend-mode').value,
          colorMode: document.getElementById('color-mode').checked,
          colorFilter: document.getElementById('color-filter').value,
          blackReplace: document.getElementById('black-replace').value,
          whiteReplace: document.getElementById('white-replace').value,
          threshold: document.getElementById('threshold').value,
          invertMono: document.getElementById('invert-mono').checked,
          blackTransparent: blackTransparent,
          whiteTransparent: whiteTransparent,
          pdfSize: document.getElementById('pdf-size').value,
          // 色適用状態も保存
          isColorApplied: currentState.isColorApplied,
          savedAt: new Date().toLocaleString('ja-JP')
        };
        await saveSlotToDB(slotNum, data);
        await renderSlotUI();
        showToast(`スロット${slotNum}に保存しました`);
      } finally {
        hideLoading();
      }
    }
    
    async function loadFromSlot(slotNum) {
      showLoading();
      try {
        const slot = await getSlotFromDB(slotNum);
        if (!slot) return;
    
        if (slot.basePdf) {
          basePdfArrayBuffer = slot.basePdf;
          basePdf = await pdfjsLib.getDocument(slot.basePdf.slice(0)).promise;
          baseCurrentPage = slot.baseCurrentPage || 1;
          document.getElementById('base-page').textContent = `${baseCurrentPage}/${basePdf.numPages}`;
        }
    
        if (slot.overlayPdf) {
          overlayPdfArrayBuffer = slot.overlayPdf;
          overlayPdf = await pdfjsLib.getDocument(slot.overlayPdf.slice(0)).promise;
          overlayCurrentPage = slot.overlayCurrentPage || 1;
          document.getElementById('overlay-page').textContent = `${overlayCurrentPage}/${overlayPdf.numPages}`;
        }
    
        currentScale = slot.currentScale;
        overlayScale = slot.overlayScale;
        overlayX = slot.overlayX;
        overlayY = slot.overlayY;
        relativePosition = slot.relativePosition;
        isPositionSet = slot.isPositionSet;
        blackTransparent = slot.blackTransparent;
        whiteTransparent = slot.whiteTransparent;
    
        // UI更新
        document.getElementById('scale').value = currentScale * 100;
        document.getElementById('scale-input').value = currentScale * 100;
        document.getElementById('overlay-scale').value = overlayScale * 100;
        document.getElementById('overlay-scale-input').value = overlayScale * 100;
        document.getElementById('opacity').value = slot.opacity;
        document.getElementById('opacity-input').value = slot.opacity;
        document.getElementById('blend-mode').value = slot.blendMode;
        document.getElementById('color-mode').checked = slot.colorMode;
        document.getElementById('color-filter').value = slot.colorFilter;
        document.getElementById('black-replace').value = slot.blackReplace;
        document.getElementById('white-replace').value = slot.whiteReplace;
        document.getElementById('threshold').value = slot.threshold;
        document.getElementById('threshold-input').value = slot.threshold;
        document.getElementById('invert-mono').checked = slot.invertMono;
        document.getElementById('pdf-size').value = slot.pdfSize;
    
        overlayCanvas.style.opacity = slot.opacity / 100;
        overlayCanvas.style.mixBlendMode = slot.blendMode;
    
        // 単色モード表示切替
        document.getElementById('normal-color-controls').style.display = slot.colorMode ? 'none' : 'block';
        document.getElementById('mono-color-controls').style.display = slot.colorMode ? 'block' : 'none';
    
        // 透明ボタン状態
        document.getElementById('black-transparent').classList.toggle('active', blackTransparent);
        document.getElementById('white-transparent').classList.toggle('active', whiteTransparent);
        document.getElementById('black-replace').style.display = blackTransparent ? 'none' : 'inline-block';
        document.getElementById('white-replace').style.display = whiteTransparent ? 'none' : 'inline-block';
        document.getElementById('black-transparent-indicator').style.display = blackTransparent ? 'inline-block' : 'none';
        document.getElementById('white-transparent-indicator').style.display = whiteTransparent ? 'inline-block' : 'none';
    
        // レンダリング
        if (basePdf) await renderBasePDF();
        if (overlayPdf) {
          await renderOverlayPDF(false);
          overlayWrapper.style.left = overlayX + 'px';
          overlayWrapper.style.top = overlayY + 'px';
          
          // 色適用状態を復元
          if (slot.isColorApplied) {
            currentState.isColorApplied = true;
            applyColorToCanvas(false);
          }
        }
      } finally {
        hideLoading();
      }
    }
    
    async function deleteSlot(slotNum, e) {
      e.stopPropagation();
      if (!confirm(`スロット${slotNum}を削除?`)) return;
      await deleteSlotFromDB(slotNum);
      await renderSlotUI();
    }
    
    async function renderSlotUI() {
      const container = document.getElementById('slotContainer');
      const slots = await getAllSlots();
      container.innerHTML = '';
      for (let i = 1; i <= SLOT_COUNT; i++) {
        const slot = slots[i];
        const group = document.createElement('div');
        group.className = 'slot-group';
        const saveBtn = document.createElement('button');
        saveBtn.className = 'slot-btn save';
        saveBtn.textContent = `S${i}`;
        saveBtn.title = slot ? `上書き保存\n${slot.savedAt}` : `スロット${i}に保存`;
        saveBtn.onclick = () => saveToSlot(i);
        const loadBtn = document.createElement('button');
        loadBtn.className = 'slot-btn load';
        loadBtn.textContent = `L${i}`;
        loadBtn.title = slot ? `読込\n${slot.savedAt}` : '(空)';
        loadBtn.onclick = () => loadFromSlot(i);
        loadBtn.disabled = !slot;
        group.appendChild(saveBtn);
        group.appendChild(loadBtn);
        if (slot) {
          const delBtn = document.createElement('button');
          delBtn.className = 'slot-btn delete';
          delBtn.textContent = '×';
          delBtn.title = '削除';
          delBtn.onclick = (e) => deleteSlot(i, e);
          group.appendChild(delBtn);
        }
        container.appendChild(group);
      }
    }
    
    let basePdf = null, overlayPdf = null;
    let baseCurrentPage = 1, overlayCurrentPage = 1;
    let currentScale = 1, overlayScale = 1;
    let overlayX = 0, overlayY = 0;
    let relativePosition = { x: 0.5, y: 0.5 };
    let isPositionSet = false;
    let scaleMatchMode = false;
    let scaleMatchPoints = { base1: null, base2: null, overlay1: null, overlay2: null };
    let currentScaleMatchStep = 0;
    let currentState = { cropData: null, colorSettings: null, isCropped: false, isColorApplied: false };
    let blackTransparent = false, whiteTransparent = false;
    let cropMode = false;
    let cropStartX = 0, cropStartY = 0, cropEndX = 0, cropEndY = 0;
    let cropRect = null, isDrawingCrop = false;
    
    const baseCanvas = document.getElementById('base-canvas');
    const overlayCanvas = document.getElementById('overlay-canvas');
    const overlayWrapper = document.getElementById('overlay-wrapper');
    const cropOverlay = document.getElementById('crop-overlay');
    const baseCtx = baseCanvas.getContext('2d');
    const overlayCtx = overlayCanvas.getContext('2d', { alpha: true });
    const cropCtx = cropOverlay.getContext('2d');
    
    overlayCtx.imageSmoothingEnabled = true;
    overlayCtx.imageSmoothingQuality = 'high';
    baseCtx.imageSmoothingEnabled = true;
    baseCtx.imageSmoothingQuality = 'high';
    
    let overlayImageData = null, originalOverlayImageData = null, fullOriginalImageData = null;
    let isResizing = false, resizeHandle = null;
    let originalWidth = 0, originalHeight = 0, resizeStartX = 0, resizeStartY = 0;
    
    function saveRelativePosition() {
      if (baseCanvas.width > 0 && baseCanvas.height > 0) {
        const centerX = overlayX + (overlayCanvas.width / 2);
        const centerY = overlayY + (overlayCanvas.height / 2);
        relativePosition.x = centerX / baseCanvas.width;
        relativePosition.y = centerY / baseCanvas.height;
        isPositionSet = true;
      }
    }
    
    function restorePositionFromRelative() {
      if (!isPositionSet) { centerOverlay(); return; }
      const centerX = relativePosition.x * baseCanvas.width;
      const centerY = relativePosition.y * baseCanvas.height;
      overlayX = centerX - (overlayCanvas.width / 2);
      overlayY = centerY - (overlayCanvas.height / 2);
      overlayWrapper.style.left = overlayX + 'px';
      overlayWrapper.style.top = overlayY + 'px';
    }
    
    document.getElementById('scale-match-mode').addEventListener('click', function() {
      scaleMatchMode = !scaleMatchMode;
      cropMode = false;
      document.getElementById('crop-mode').classList.remove('active');
      this.classList.toggle('active');
      document.getElementById('canvas-container').classList.toggle('scale-match-active');
      document.getElementById('scale-match-info').classList.toggle('active');
      if (scaleMatchMode) { currentScaleMatchStep = 0; updateScaleMatchStatus(); }
      else { clearScaleMatchPoints(); }
    });
    
    document.getElementById('scale-match-reset').addEventListener('click', () => {
      clearScaleMatchPoints();
      currentScaleMatchStep = 0;
      if (scaleMatchMode) updateScaleMatchStatus();
    });
    
    function clearScaleMatchPoints() {
      scaleMatchPoints = { base1: null, base2: null, overlay1: null, overlay2: null };
      document.querySelectorAll('.point-marker').forEach(m => m.remove());
    }
    
    function updateScaleMatchStatus() {
      const statusEl = document.getElementById('scale-match-status');
      const messages = ['ベース図面の1点目を選択','ベース図面の2点目を選択','重ねる図面の1点目を選択','重ねる図面の2点目を選択'];
      if (currentScaleMatchStep < messages.length) statusEl.textContent = messages[currentScaleMatchStep];
      else { statusEl.textContent = '縮尺を計算中...'; calculateAndApplyScale(); }
    }
    
    function addPointMarker(x, y, className) {
      const marker = document.createElement('div');
      marker.className = `point-marker ${className}`;
      marker.style.left = (x - 5) + 'px';
      marker.style.top = (y - 5) + 'px';
      document.getElementById('canvas-container').appendChild(marker);
    }
    
    baseCanvas.addEventListener('click', (e) => {
      if (!scaleMatchMode || currentScaleMatchStep >= 2) return;
      const rect = baseCanvas.getBoundingClientRect();
      const containerRect = document.getElementById('canvas-container').getBoundingClientRect();
      const x = e.clientX - containerRect.left, y = e.clientY - containerRect.top;
      if (currentScaleMatchStep === 0) { scaleMatchPoints.base1 = { x: e.clientX - rect.left, y: e.clientY - rect.top }; addPointMarker(x, y, 'base-1'); }
      else if (currentScaleMatchStep === 1) { scaleMatchPoints.base2 = { x: e.clientX - rect.left, y: e.clientY - rect.top }; addPointMarker(x, y, 'base-2'); }
      currentScaleMatchStep++;
      updateScaleMatchStatus();
    });
    
    overlayCanvas.addEventListener('click', (e) => {
      if (!scaleMatchMode || currentScaleMatchStep < 2 || currentScaleMatchStep >= 4) return;
      const rect = overlayCanvas.getBoundingClientRect();
      const containerRect = document.getElementById('canvas-container').getBoundingClientRect();
      const x = e.clientX - containerRect.left, y = e.clientY - containerRect.top;
      if (currentScaleMatchStep === 2) { scaleMatchPoints.overlay1 = { x: e.clientX - rect.left, y: e.clientY - rect.top }; addPointMarker(x, y, 'overlay-1'); }
      else if (currentScaleMatchStep === 3) { scaleMatchPoints.overlay2 = { x: e.clientX - rect.left, y: e.clientY - rect.top }; addPointMarker(x, y, 'overlay-2'); }
      currentScaleMatchStep++;
      updateScaleMatchStatus();
    });
    
    async function calculateAndApplyScale() {
      const baseDistance = Math.sqrt(Math.pow(scaleMatchPoints.base2.x - scaleMatchPoints.base1.x, 2) + Math.pow(scaleMatchPoints.base2.y - scaleMatchPoints.base1.y, 2));
      const overlayDistance = Math.sqrt(Math.pow(scaleMatchPoints.overlay2.x - scaleMatchPoints.overlay1.x, 2) + Math.pow(scaleMatchPoints.overlay2.y - scaleMatchPoints.overlay1.y, 2));
      const newScale = (baseDistance / overlayDistance) * overlayScale;
      const scalePercent = Math.round(newScale * 100);
      document.getElementById('overlay-scale').value = Math.min(500, Math.max(10, scalePercent));
      document.getElementById('overlay-scale-input').value = scalePercent;
      overlayScale = newScale;
      await renderOverlayPDF(true);
      const scaledOverlayX = scaleMatchPoints.overlay1.x * (newScale / overlayScale);
      const scaledOverlayY = scaleMatchPoints.overlay1.y * (newScale / overlayScale);
      overlayX = scaleMatchPoints.base1.x - scaledOverlayX;
      overlayY = scaleMatchPoints.base1.y - scaledOverlayY;
      overlayWrapper.style.left = overlayX + 'px';
      overlayWrapper.style.top = overlayY + 'px';
      saveRelativePosition();
      scaleMatchMode = false;
      document.getElementById('scale-match-mode').classList.remove('active');
      document.getElementById('canvas-container').classList.remove('scale-match-active');
      document.getElementById('scale-match-info').classList.remove('active');
      clearScaleMatchPoints();
      document.getElementById('scale-match-status').textContent = '縮尺合わせ完了';
    }
    
    function setupSliderInputSync(sliderId, inputId, callback) {
      const slider = document.getElementById(sliderId);
      const input = document.getElementById(inputId);
      slider.addEventListener('input', (e) => { input.value = e.target.value; callback(e.target.value); });
      input.addEventListener('input', (e) => {
        let value = parseInt(e.target.value);
        if (isNaN(value)) return;
        value = Math.max(parseInt(input.min), Math.min(parseInt(input.max), value));
        e.target.value = value; slider.value = value; callback(value);
      });
      input.addEventListener('blur', (e) => { if (e.target.value === '') e.target.value = slider.value; });
    }
    
    setupSliderInputSync('opacity', 'opacity-input', (value) => { overlayCanvas.style.opacity = value / 100; });
    setupSliderInputSync('threshold', 'threshold-input', () => {});
    setupSliderInputSync('scale', 'scale-input', async (value) => { saveRelativePosition(); currentScale = value / 100; await renderBasePDF(); await renderOverlayPDF(true); restorePositionFromRelative(); });
    setupSliderInputSync('overlay-scale', 'overlay-scale-input', async (value) => { saveRelativePosition(); overlayScale = value / 100; await renderOverlayPDF(true); restorePositionFromRelative(); });
    
    document.querySelectorAll('.resize-handle').forEach(handle => { handle.addEventListener('mousedown', startResize); });
    
    function startResize(e) {
      isResizing = true; resizeHandle = e.target.classList[1];
      resizeStartX = e.clientX; resizeStartY = e.clientY;
      originalWidth = overlayCanvas.width; originalHeight = overlayCanvas.height;
      document.addEventListener('mousemove', doResize);
      document.addEventListener('mouseup', stopResize);
      e.preventDefault(); e.stopPropagation();
    }
    
    async function doResize(e) {
      if (!isResizing) return;
      const deltaX = e.clientX - resizeStartX;
      let newWidth = originalWidth;
      const aspectRatio = originalWidth / originalHeight;
      const centerX = overlayX + originalWidth / 2, centerY = overlayY + originalHeight / 2;
      switch(resizeHandle) {
        case 'se': newWidth = originalWidth + deltaX; break;
        case 'sw': newWidth = originalWidth - deltaX; overlayX = centerX - newWidth / 2; break;
        case 'ne': newWidth = originalWidth + deltaX; overlayY = centerY - (newWidth / aspectRatio) / 2; break;
        case 'nw': newWidth = originalWidth - deltaX; overlayX = centerX - newWidth / 2; overlayY = centerY - (newWidth / aspectRatio) / 2; break;
      }
      if (newWidth < 50) return;
      const baseWidth = currentState.isCropped ? currentState.cropData.width : (fullOriginalImageData ? fullOriginalImageData.width / (currentScale * 2) : overlayCanvas.width);
      const newScale = (newWidth / (baseWidth * currentScale)) * 100;
      document.getElementById('overlay-scale').value = Math.min(500, Math.max(10, newScale));
      document.getElementById('overlay-scale-input').value = Math.round(newScale);
      saveRelativePosition(); overlayScale = newScale / 100; await renderOverlayPDF(true);
      overlayWrapper.style.left = overlayX + 'px'; overlayWrapper.style.top = overlayY + 'px';
    }
    
    async function stopResize() { isResizing = false; resizeHandle = null; saveRelativePosition(); document.removeEventListener('mousemove', doResize); document.removeEventListener('mouseup', stopResize); }
    
    async function renderBasePDF() {
      if (!basePdf) return;
      const page = await basePdf.getPage(baseCurrentPage);
      const viewport = page.getViewport({ scale: currentScale });
      baseCanvas.width = viewport.width; baseCanvas.height = viewport.height;
      await page.render({ canvasContext: baseCtx, viewport: viewport, intent: 'display' }).promise;
      updateContainerSize();
    }
    
    async function renderOverlayPDF(preserveState = false) {
      if (!overlayPdf) return;
      const page = await overlayPdf.getPage(overlayCurrentPage);
      const renderScale = 2;
      const viewport = page.getViewport({ scale: currentScale * overlayScale * renderScale });
      const tempCanvas = document.createElement('canvas');
      tempCanvas.width = viewport.width; tempCanvas.height = viewport.height;
      const tempCtx = tempCanvas.getContext('2d');
      tempCtx.imageSmoothingEnabled = true; tempCtx.imageSmoothingQuality = 'high';
      await page.render({ canvasContext: tempCtx, viewport: viewport, intent: 'display' }).promise;
      const actualWidth = viewport.width / renderScale, actualHeight = viewport.height / renderScale;
      fullOriginalImageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
      if (preserveState && currentState.isCropped && currentState.cropData) {
        const scale = currentScale * overlayScale * renderScale;
        const cropX = currentState.cropData.x * scale, cropY = currentState.cropData.y * scale;
        const cropWidth = currentState.cropData.width * scale, cropHeight = currentState.cropData.height * scale;
        overlayCanvas.width = cropWidth / renderScale; overlayCanvas.height = cropHeight / renderScale;
        overlayWrapper.style.width = (cropWidth / renderScale) + 'px'; overlayWrapper.style.height = (cropHeight / renderScale) + 'px';
        overlayCtx.drawImage(tempCanvas, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth / renderScale, cropHeight / renderScale);
      } else {
        overlayCanvas.width = actualWidth; overlayCanvas.height = actualHeight;
        overlayWrapper.style.width = actualWidth + 'px'; overlayWrapper.style.height = actualHeight + 'px';
        overlayCtx.drawImage(tempCanvas, 0, 0, actualWidth, actualHeight);
      }
      overlayImageData = overlayCtx.getImageData(0, 0, overlayCanvas.width, overlayCanvas.height);
      originalOverlayImageData = overlayCtx.getImageData(0, 0, overlayCanvas.width, overlayCanvas.height);
      if (preserveState && currentState.isColorApplied) applyColorToCanvas(false);
    }
    
    function updateContainerSize() {
      const container = document.getElementById('canvas-container');
      container.style.width = baseCanvas.width + 'px'; container.style.height = baseCanvas.height + 'px';
      cropOverlay.width = baseCanvas.width; cropOverlay.height = baseCanvas.height;
    }
    
    function centerOverlay() {
      overlayX = (baseCanvas.width - overlayCanvas.width) / 2;
      overlayY = (baseCanvas.height - overlayCanvas.height) / 2;
      overlayWrapper.style.left = overlayX + 'px'; overlayWrapper.style.top = overlayY + 'px';
      saveRelativePosition();
    }
    
    document.getElementById('crop-mode').addEventListener('click', function() {
      cropMode = !cropMode; scaleMatchMode = false;
      document.getElementById('scale-match-mode').classList.remove('active');
      this.classList.toggle('active');
      document.getElementById('canvas-container').classList.toggle('crop-active');
      document.getElementById('canvas-container').classList.remove('scale-match-active');
      document.getElementById('crop-info').style.display = cropMode ? 'block' : 'none';
      document.getElementById('scale-match-info').classList.remove('active');
      if (!cropMode) clearCropOverlay();
    });
    
    overlayCanvas.addEventListener('mousedown', (e) => {
      if (scaleMatchMode) return;
      if (!cropMode) { startDrag(e); return; }
      const rect = overlayCanvas.getBoundingClientRect();
      cropStartX = e.clientX - rect.left; cropStartY = e.clientY - rect.top;
      isDrawingCrop = true;
      document.addEventListener('mousemove', drawCropSelection);
      document.addEventListener('mouseup', endCropSelection);
    });
    
    function drawCropSelection(e) {
      if (!isDrawingCrop) return;
      const rect = overlayCanvas.getBoundingClientRect();
      cropEndX = e.clientX - rect.left; cropEndY = e.clientY - rect.top;
      cropCtx.clearRect(0, 0, cropOverlay.width, cropOverlay.height);
      const x = Math.min(cropStartX, cropEndX) + overlayX, y = Math.min(cropStartY, cropEndY) + overlayY;
      const width = Math.abs(cropEndX - cropStartX), height = Math.abs(cropEndY - cropStartY);
      cropCtx.strokeStyle = '#ff0000'; cropCtx.lineWidth = 2; cropCtx.setLineDash([5, 5]);
      cropCtx.strokeRect(x, y, width, height);
      cropRect = { x: Math.min(cropStartX, cropEndX), y: Math.min(cropStartY, cropEndY), width, height };
    }
    
    function endCropSelection() { if (!isDrawingCrop) return; document.removeEventListener('mousemove', drawCropSelection); document.removeEventListener('mouseup', endCropSelection); isDrawingCrop = false; }
    
    document.getElementById('apply-crop').addEventListener('click', () => {
      if (!cropRect || !overlayImageData) return;
      saveRelativePosition();
      currentState.cropData = { x: cropRect.x / (currentScale * overlayScale), y: cropRect.y / (currentScale * overlayScale), width: cropRect.width / (currentScale * overlayScale), height: cropRect.height / (currentScale * overlayScale) };
      currentState.isCropped = true;
      const tempCanvas = document.createElement('canvas');
      tempCanvas.width = cropRect.width; tempCanvas.height = cropRect.height;
      const tempCtx = tempCanvas.getContext('2d');
      tempCtx.drawImage(overlayCanvas, cropRect.x, cropRect.y, cropRect.width, cropRect.height, 0, 0, cropRect.width, cropRect.height);
      overlayCanvas.width = cropRect.width; overlayCanvas.height = cropRect.height;
      overlayWrapper.style.width = cropRect.width + 'px'; overlayWrapper.style.height = cropRect.height + 'px';
      overlayCtx.drawImage(tempCanvas, 0, 0);
      overlayImageData = overlayCtx.getImageData(0, 0, overlayCanvas.width, overlayCanvas.height);
      originalOverlayImageData = overlayCtx.getImageData(0, 0, overlayCanvas.width, overlayCanvas.height);
      if (currentState.isColorApplied) applyColorToCanvas(false);
      restorePositionFromRelative();
      clearCropOverlay(); cropMode = false; cropRect = null;
      document.getElementById('crop-mode').classList.remove('active');
      document.getElementById('canvas-container').classList.remove('crop-active');
      document.getElementById('crop-info').style.display = 'none';
    });
    
    document.getElementById('reset-crop').addEventListener('click', async () => {
      cropRect = null; currentState.cropData = null; currentState.isCropped = false;
      clearCropOverlay(); saveRelativePosition();
      if (overlayPdf) { await renderOverlayPDF(false); if (currentState.isColorApplied) applyColorToCanvas(false); restorePositionFromRelative(); }
    });
    
    function clearCropOverlay() { cropCtx.clearRect(0, 0, cropOverlay.width, cropOverlay.height); }
    
    document.getElementById('base-file').addEventListener('change', async (e) => {
      const file = e.target.files[0]; if (!file) return;
      basePdfArrayBuffer = await file.arrayBuffer();
      basePdf = await pdfjsLib.getDocument(basePdfArrayBuffer.slice(0)).promise;
      baseCurrentPage = 1;
      document.getElementById('base-page').textContent = `${baseCurrentPage}/${basePdf.numPages}`;
      await renderBasePDF();
      if (overlayPdf) restorePositionFromRelative();
    });
    
    document.getElementById('overlay-file').addEventListener('change', async (e) => {
      const file = e.target.files[0]; if (!file) return;
      overlayPdfArrayBuffer = await file.arrayBuffer();
      overlayPdf = await pdfjsLib.getDocument(overlayPdfArrayBuffer.slice(0)).promise;
      overlayCurrentPage = 1;
      currentState = { cropData: null, colorSettings: null, isCropped: false, isColorApplied: false };
      cropRect = null; isPositionSet = false;
      document.getElementById('overlay-page').textContent = `${overlayCurrentPage}/${overlayPdf.numPages}`;
      await renderOverlayPDF(false); centerOverlay();
    });
    
    document.getElementById('base-prev').addEventListener('click', async () => { if (!basePdf || baseCurrentPage <= 1) return; baseCurrentPage--; document.getElementById('base-page').textContent = `${baseCurrentPage}/${basePdf.numPages}`; await renderBasePDF(); if (overlayPdf) restorePositionFromRelative(); });
    document.getElementById('base-next').addEventListener('click', async () => { if (!basePdf || baseCurrentPage >= basePdf.numPages) return; baseCurrentPage++; document.getElementById('base-page').textContent = `${baseCurrentPage}/${basePdf.numPages}`; await renderBasePDF(); if (overlayPdf) restorePositionFromRelative(); });
    document.getElementById('overlay-prev').addEventListener('click', async () => { if (!overlayPdf || overlayCurrentPage <= 1) return; overlayCurrentPage--; currentState = { cropData: null, colorSettings: null, isCropped: false, isColorApplied: false }; cropRect = null; document.getElementById('overlay-page').textContent = `${overlayCurrentPage}/${overlayPdf.numPages}`; await renderOverlayPDF(false); restorePositionFromRelative(); });
    document.getElementById('overlay-next').addEventListener('click', async () => { if (!overlayPdf || overlayCurrentPage >= overlayPdf.numPages) return; overlayCurrentPage++; currentState = { cropData: null, colorSettings: null, isCropped: false, isColorApplied: false }; cropRect = null; document.getElementById('overlay-page').textContent = `${overlayCurrentPage}/${overlayPdf.numPages}`; await renderOverlayPDF(false); restorePositionFromRelative(); });
    
    document.getElementById('color-mode').addEventListener('change', (e) => {
      const isMonoMode = e.target.checked;
      document.getElementById('normal-color-controls').style.display = isMonoMode ? 'none' : 'block';
      document.getElementById('mono-color-controls').style.display = isMonoMode ? 'block' : 'none';
    });
    
    document.getElementById('black-transparent').addEventListener('click', function() {
      blackTransparent = !blackTransparent; this.classList.toggle('active');
      document.getElementById('black-replace').style.display = blackTransparent ? 'none' : 'inline-block';
      document.getElementById('black-transparent-indicator').style.display = blackTransparent ? 'inline-block' : 'none';
      if (document.getElementById('color-mode').checked) applyColorToCanvas(true);
    });
    
    document.getElementById('white-transparent').addEventListener('click', function() {
      whiteTransparent = !whiteTransparent; this.classList.toggle('active');
      document.getElementById('white-replace').style.display = whiteTransparent ? 'none' : 'inline-block';
      document.getElementById('white-transparent-indicator').style.display = whiteTransparent ? 'inline-block' : 'none';
      if (document.getElementById('color-mode').checked) applyColorToCanvas(true);
    });
    
    function applyColorToCanvas(saveState = true) {
      if (!overlayImageData) return;
      const colorMode = document.getElementById('color-mode').checked;
      if (saveState) {
        currentState.colorSettings = { colorMode, normalColor: document.getElementById('color-filter').value, blackColor: document.getElementById('black-replace').value, whiteColor: document.getElementById('white-replace').value, threshold: document.getElementById('threshold').value, invert: document.getElementById('invert-mono').checked, blackTransparent, whiteTransparent };
        currentState.isColorApplied = true;
      }
      const newImageData = new ImageData(new Uint8ClampedArray(originalOverlayImageData.data), originalOverlayImageData.width, originalOverlayImageData.height);
      if (colorMode) {
        const blackColor = document.getElementById('black-replace').value, whiteColor = document.getElementById('white-replace').value;
        const threshold = parseInt(document.getElementById('threshold').value), invert = document.getElementById('invert-mono').checked;
        const blackR = parseInt(blackColor.substr(1, 2), 16), blackG = parseInt(blackColor.substr(3, 2), 16), blackB = parseInt(blackColor.substr(5, 2), 16);
        const whiteR = parseInt(whiteColor.substr(1, 2), 16), whiteG = parseInt(whiteColor.substr(3, 2), 16), whiteB = parseInt(whiteColor.substr(5, 2), 16);
        for (let i = 0; i < newImageData.data.length; i += 4) {
          if (newImageData.data[i + 3] > 0) {
            const gray = newImageData.data[i] * 0.299 + newImageData.data[i + 1] * 0.587 + newImageData.data[i + 2] * 0.114;
            let isBlack = gray < threshold; if (invert) isBlack = !isBlack;
            if (isBlack) { if (blackTransparent) newImageData.data[i + 3] = 0; else { newImageData.data[i] = blackR; newImageData.data[i + 1] = blackG; newImageData.data[i + 2] = blackB; } }
            else { if (whiteTransparent) newImageData.data[i + 3] = 0; else { newImageData.data[i] = whiteR; newImageData.data[i + 1] = whiteG; newImageData.data[i + 2] = whiteB; } }
          }
        }
      } else {
        const color = document.getElementById('color-filter').value;
        const r = parseInt(color.substr(1, 2), 16), g = parseInt(color.substr(3, 2), 16), b = parseInt(color.substr(5, 2), 16);
        for (let i = 0; i < newImageData.data.length; i += 4) {
          if (newImageData.data[i + 3] > 0) {
            newImageData.data[i] = Math.min(255, newImageData.data[i] * (r / 128));
            newImageData.data[i + 1] = Math.min(255, newImageData.data[i + 1] * (g / 128));
            newImageData.data[i + 2] = Math.min(255, newImageData.data[i + 2] * (b / 128));
          }
        }
      }
      overlayCtx.putImageData(newImageData, 0, 0); overlayImageData = newImageData;
    }
    
    document.getElementById('apply-color').addEventListener('click', () => applyColorToCanvas(true));
    
    document.getElementById('reset-color').addEventListener('click', () => {
      if (originalOverlayImageData) { overlayCtx.putImageData(originalOverlayImageData, 0, 0); overlayImageData = originalOverlayImageData; }
      currentState.colorSettings = null; currentState.isColorApplied = false;
      blackTransparent = false; whiteTransparent = false;
      document.getElementById('black-transparent').classList.remove('active');
      document.getElementById('white-transparent').classList.remove('active');
      document.getElementById('black-replace').style.display = 'inline-block';
      document.getElementById('white-replace').style.display = 'inline-block';
      document.getElementById('black-transparent-indicator').style.display = 'none';
      document.getElementById('white-transparent-indicator').style.display = 'none';
    });
    
    document.querySelectorAll('.preset-color').forEach(preset => {
      preset.addEventListener('click', () => {
        const color = preset.dataset.color;
        if (document.getElementById('color-mode').checked) { if (!blackTransparent) document.getElementById('black-replace').value = color; }
        else { document.getElementById('color-filter').value = color; }
        applyColorToCanvas(true);
      });
    });
    
    ['black-replace', 'white-replace', 'invert-mono'].forEach(id => { document.getElementById(id).addEventListener('change', () => { if (document.getElementById('color-mode').checked) applyColorToCanvas(true); }); });
    document.getElementById('threshold').addEventListener('input', () => { if (document.getElementById('color-mode').checked) applyColorToCanvas(true); });
    
    let isDragging = false, dragStartX, dragStartY, initialLeft, initialTop;
    
    function startDrag(e) {
      if (cropMode || isResizing || scaleMatchMode) return;
      isDragging = true; dragStartX = e.clientX; dragStartY = e.clientY;
      const rect = overlayWrapper.getBoundingClientRect();
      const containerRect = document.getElementById('canvas-container').getBoundingClientRect();
      initialLeft = rect.left - containerRect.left; initialTop = rect.top - containerRect.top;
      document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDrag);
      e.preventDefault();
    }
    
    function drag(e) { if (!isDragging) return; overlayX = initialLeft + (e.clientX - dragStartX); overlayY = initialTop + (e.clientY - dragStartY); overlayWrapper.style.left = overlayX + 'px'; overlayWrapper.style.top = overlayY + 'px'; }
    function stopDrag() { isDragging = false; saveRelativePosition(); document.removeEventListener('mousemove', drag); document.removeEventListener('mouseup', stopDrag); }
    
    document.getElementById('reset-position').addEventListener('click', () => centerOverlay());
    document.getElementById('fit-to-base').addEventListener('click', () => { overlayX = 0; overlayY = 0; overlayWrapper.style.left = '0px'; overlayWrapper.style.top = '0px'; saveRelativePosition(); });
    document.getElementById('blend-mode').addEventListener('change', (e) => { overlayCanvas.style.mixBlendMode = e.target.value; });
    
    function exportAsImage(format) {
      const exportScale = 5;
      const exportCanvas = document.createElement('canvas');
      exportCanvas.width = baseCanvas.width * exportScale; exportCanvas.height = baseCanvas.height * exportScale;
      const exportCtx = exportCanvas.getContext('2d');
      exportCtx.imageSmoothingEnabled = true; exportCtx.imageSmoothingQuality = 'high';
      if (format === 'jpeg') { exportCtx.fillStyle = 'white'; exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height); }
      exportCtx.scale(exportScale, exportScale);
      exportCtx.drawImage(baseCanvas, 0, 0, baseCanvas.width, baseCanvas.height);
      exportCtx.globalAlpha = parseFloat(overlayCanvas.style.opacity || 1);
      exportCtx.globalCompositeOperation = document.getElementById('blend-mode').value;
      exportCtx.drawImage(overlayCanvas, overlayX, overlayY, overlayCanvas.width, overlayCanvas.height);
      exportCanvas.toBlob((blob) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `pdf-kasane-zu_${Date.now()}.${format === 'jpeg' ? 'jpg' : format}`; a.click(); URL.revokeObjectURL(url); }, `image/${format}`, 1.0);
    }
    
    document.getElementById('export-jpg').addEventListener('click', () => exportAsImage('jpeg'));
    document.getElementById('export-png').addEventListener('click', () => exportAsImage('png'));
    
    document.getElementById('export-pdf').addEventListener('click', () => {
      if (!window.jspdf) { alert('PDF出力ライブラリの読み込みに失敗しました。'); return; }
      const { jsPDF } = window.jspdf;
      const pdfSize = document.getElementById('pdf-size').value;
      const exportScale = 4;
      const exportCanvas = document.createElement('canvas');
      exportCanvas.width = baseCanvas.width * exportScale; exportCanvas.height = baseCanvas.height * exportScale;
      const exportCtx = exportCanvas.getContext('2d');
      exportCtx.imageSmoothingEnabled = true; exportCtx.imageSmoothingQuality = 'high';
      exportCtx.scale(exportScale, exportScale);
      exportCtx.fillStyle = 'white'; exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
      exportCtx.drawImage(baseCanvas, 0, 0, baseCanvas.width, baseCanvas.height);
      exportCtx.globalAlpha = parseFloat(overlayCanvas.style.opacity || 1);
      exportCtx.globalCompositeOperation = document.getElementById('blend-mode').value;
      exportCtx.drawImage(overlayCanvas, overlayX, overlayY, overlayCanvas.width, overlayCanvas.height);
      const orientation = exportCanvas.width > exportCanvas.height ? 'l' : 'p';
      const pdf = new jsPDF({ orientation, unit: 'mm', format: pdfSize, compress: false });
      const pageWidth = pdf.internal.pageSize.getWidth(), pageHeight = pdf.internal.pageSize.getHeight();
      const imgAspect = exportCanvas.width / exportCanvas.height, pageAspect = pageWidth / pageHeight;
      let imgWidth, imgHeight;
      if (imgAspect > pageAspect) { imgWidth = pageWidth; imgHeight = pageWidth / imgAspect; }
      else { imgHeight = pageHeight; imgWidth = pageHeight * imgAspect; }
      const x = (pageWidth - imgWidth) / 2, y = (pageHeight - imgHeight) / 2;
      pdf.addImage(exportCanvas.toDataURL('image/png', 1.0), 'PNG', x, y, imgWidth, imgHeight, undefined, 'NONE');
      pdf.save(`pdf-kasane-zu_${pdfSize.toUpperCase()}_${Date.now()}.pdf`);
    });
    
    document.getElementById('canvas-container').addEventListener('wheel', async (e) => {
      if (e.ctrlKey) {
        e.preventDefault();
        const delta = e.deltaY > 0 ? -5 : 5;
        let newValue = Math.max(50, Math.min(500, parseInt(document.getElementById('scale').value) + delta));
        document.getElementById('scale').value = newValue; document.getElementById('scale-input').value = newValue;
        saveRelativePosition(); currentScale = newValue / 100;
        await renderBasePDF(); await renderOverlayPDF(true); restorePositionFromRelative();
      }
    });
    
    async function init() { await initDB(); await renderSlotUI(); overlayCanvas.style.opacity = 0.5; }
    init();
    </script>
    
    </body>
    </html>
    

    ⑥ PDF計測

    <!DOCTYPE html>
    <html lang="ja">
    <head>
    <style>
      .preloader {
        position: fixed;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        background: #fff;
        display: flex;
        justify-content: center;
        align-items: center;
        gap: 20px;
        z-index: 9999;
        animation: fadeOut 0.1s 0.3s forwards;
      }
      .glasses {
        display: flex;
        align-items: center;
        animation: swing 0.3s ease-in-out infinite alternate;
        transform-origin: top center;
      }
      .lens {
        width: 40px;
        height: 20px;
        border: 3px solid #888;
        border-radius: 4px;
        background: rgba(200,200,200,0.2);
      }
      .bridge {
        width: 12px;
        height: 3px;
        background: #888;
      }
      .temple {
        width: 20px;
        height: 3px;
        background: #888;
      }
      .pencil {
        font-size: 48px;
        animation: swing 0.3s ease-in-out infinite alternate;
        animation-delay: 0.15s;
        transform-origin: top center;
        display: inline-block;
      }
      @keyframes swing {
        0% { transform: rotate(-15deg); }
        100% { transform: rotate(15deg); }
      }
      @keyframes fadeOut {
        to { opacity: 0; visibility: hidden; }
      }
    </style>
    <div class="preloader">
      <div class="glasses">
        <div class="temple"></div>
        <div class="lens"></div>
        <div class="bridge"></div>
        <div class="lens"></div>
        <div class="temple"></div>
      </div>
      <span class="pencil">✏️</span>
    </div>
    
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>PDF重ね図</title>
    
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
    <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: 'Arial', sans-serif; background: #f0f0f0; }
    .container { display: flex; height: 100vh; }
    .sidebar { width: 300px; background: #2c3e50; color: white; padding: 20px; overflow-y: auto; }
    .title-row { display: flex; align-items: flex-end; gap: 10px; margin-bottom: 15px; flex-wrap: wrap; }
    .title-row h2 { margin: 0; }
    .slot-wrapper { display: flex; flex-direction: column; align-items: flex-start; }
    .slot-label { font-size: 9px; color: #95a5a6; margin-bottom: 2px; }
    .slot-container { display: flex; gap: 2px; align-items: center; }
    .slot-group { position: relative; display: flex; gap: 1px; }
    .slot-btn { padding: 4px 6px; font-size: 10px; border-radius: 3px; width: auto; margin: 0; }
    .slot-btn.save { background: #27ae60; }
    .slot-btn.save:hover { background: #229954; }
    .slot-btn.load { background: #3498db; }
    .slot-btn.load:hover { background: #2980b9; }
    .slot-btn.load:disabled { background: #555; cursor: not-allowed; }
    .slot-btn.delete { position: absolute; top: -5px; right: -5px; background: #c0392b; padding: 1px 4px; font-size: 8px; border-radius: 50%; min-width: auto; line-height: 1; }
    .slot-btn.delete:hover { background: #a93226; }
    .control-group { margin-bottom: 20px; padding-bottom: 20px; border-bottom: 1px solid #34495e; }
    .control-group h3 { margin-bottom: 10px; font-size: 14px; }
    .file-input { width: 100%; padding: 10px; margin-bottom: 10px; background: #34495e; border: 1px solid #4a5f7a; color: white; cursor: pointer; }
    .slider-container { display: flex; align-items: center; margin: 10px 0; }
    .slider { flex: 1; margin: 0 10px; }
    .scale-input { width: 60px; padding: 5px; background: #34495e; border: 1px solid #4a5f7a; color: white; text-align: center; margin-left: 5px; }
    .scale-input:focus { outline: none; border-color: #3498db; }
    .scale-unit { margin-left: 5px; color: #95a5a6; }
    button { width: 100%; padding: 10px; margin: 5px 0; background: #3498db; color: white; border: none; cursor: pointer; transition: background 0.3s; }
    button:hover { background: #2980b9; }
    button.active { background: #e74c3c; }
    .export-btn { background: #27ae60; }
    .export-btn:hover { background: #229954; }
    .main-area { flex: 1; position: relative; overflow: auto; background: #ecf0f1; }
    #canvas-container { position: relative; margin: 20px; display: inline-block; background: white; background-image: linear-gradient(45deg, #e0e0e0 25%, transparent 25%), linear-gradient(-45deg, #e0e0e0 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #e0e0e0 75%), linear-gradient(-45deg, transparent 75%, #e0e0e0 75%); background-size: 20px 20px; background-position: 0 0, 0 10px, 10px -10px, -10px 0px; }
    .pdf-canvas { position: absolute; top: 0; left: 0; border: 1px solid #bdc3c7; }
    #base-canvas { z-index: 1; position: relative; }
    #overlay-wrapper { position: absolute; z-index: 2; }
    #overlay-canvas { cursor: move; display: block; image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges; }
    .resize-handle { position: absolute; width: 10px; height: 10px; background: #3498db; border: 2px solid white; display: none; z-index: 10; }
    #overlay-wrapper:hover .resize-handle { display: block; }
    .resize-handle:hover { background: #e74c3c; }
    .resize-handle.nw { top: -5px; left: -5px; cursor: nw-resize; }
    .resize-handle.ne { top: -5px; right: -5px; cursor: ne-resize; }
    .resize-handle.sw { bottom: -5px; left: -5px; cursor: sw-resize; }
    .resize-handle.se { bottom: -5px; right: -5px; cursor: se-resize; }
    #crop-overlay { z-index: 3; pointer-events: none; position: absolute; top: 0; left: 0; }
    .crop-active #overlay-canvas { cursor: crosshair !important; }
    .scale-match-active #base-canvas { cursor: crosshair !important; }
    .scale-match-active #overlay-canvas { cursor: crosshair !important; }
    .scale-match-info { padding: 10px; background: #34495e; border-radius: 5px; margin-top: 10px; font-size: 12px; display: none; }
    .scale-match-info.active { display: block; }
    .scale-match-status { padding: 5px; margin-top: 5px; background: #2c3e50; border-radius: 3px; font-size: 11px; }
    .point-marker { position: absolute; width: 10px; height: 10px; border-radius: 50%; z-index: 10; pointer-events: none; }
    .point-marker.base-1 { background: #3498db; border: 2px solid white; }
    .point-marker.base-2 { background: #3498db; border: 2px solid white; }
    .point-marker.overlay-1 { background: #e74c3c; border: 2px solid white; }
    .point-marker.overlay-2 { background: #e74c3c; border: 2px solid white; }
    .color-controls { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; }
    .color-input { width: 50px; height: 30px; border: none; cursor: pointer; }
    .transparent-btn { padding: 5px 10px; margin-left: 5px; background: #7f8c8d; font-size: 12px; width: auto; }
    .transparent-btn.active { background: #e74c3c; }
    .page-controls { display: flex; gap: 10px; align-items: center; justify-content: center; }
    .page-info { padding: 0 10px; }
    .preset-colors { display: flex; gap: 5px; margin-top: 10px; }
    .preset-color { width: 30px; height: 30px; border: 2px solid #34495e; cursor: pointer; }
    .preset-color:hover { border-color: white; }
    .mono-options { margin-top: 10px; padding: 10px; background: #34495e; border-radius: 5px; }
    .threshold-container { margin: 10px 0; }
    .color-row { display: flex; align-items: center; margin-bottom: 10px; }
    .color-label { width: 40px; }
    .transparent-indicator { width: 50px; height: 30px; border: 1px solid #fff; background-image: linear-gradient(45deg, #666 25%, transparent 25%), linear-gradient(-45deg, #666 25%, transparent 25%), linear-gradient(45deg, transparent 75%, #666 75%), linear-gradient(-45deg, transparent 75%, #666 75%); background-size: 10px 10px; background-position: 0 0, 0 5px, 5px -5px, -5px 0px; display: none; margin-left: 10px; }
    .crop-info { padding: 10px; background: #34495e; border-radius: 5px; margin-top: 10px; font-size: 12px; }
    .quality-info { padding: 10px; background: #34495e; border-radius: 5px; margin-top: 10px; font-size: 11px; color: #95a5a6; }
    .pdf-size-select { width: 100%; padding: 5px; margin: 10px 0; background: #34495e; border: 1px solid #4a5f7a; color: white; }
    .loading-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); display: flex; justify-content: center; align-items: center; z-index: 9999; color: white; font-size: 18px; }
    .loading-overlay.hidden { display: none; }
    .toast { position: fixed; top: 20px; left: 50%; transform: translateX(-50%); background: #27ae60; color: white; padding: 12px 24px; border-radius: 6px; font-size: 14px; z-index: 10000; opacity: 0; transition: opacity 0.3s; pointer-events: none; }
    .toast.show { opacity: 1; }
    </style>
    </head>
    <body>
    
    <div class="loading-overlay hidden" id="loading-overlay"><div>読み込み中...</div></div>
    <div class="toast" id="toast"></div>
    
    <div class="container">
      <div class="sidebar">
        <div class="title-row">
          <h2>PDF重ね図</h2>
          <div class="slot-wrapper">
            <span class="slot-label">保存スロット</span>
            <div class="slot-container" id="slotContainer"></div>
          </div>
        </div>
        
        <div class="control-group">
          <h3>ベースPDF</h3>
          <input type="file" id="base-file" class="file-input" accept=".pdf">
          <div class="page-controls">
            <button id="base-prev">◀</button>
            <span class="page-info" id="base-page">1/1</span>
            <button id="base-next">▶</button>
          </div>
        </div>
        
        <div class="control-group">
          <h3>重ねるPDF</h3>
          <input type="file" id="overlay-file" class="file-input" accept=".pdf">
          <div class="page-controls">
            <button id="overlay-prev">◀</button>
            <span class="page-info" id="overlay-page">1/1</span>
            <button id="overlay-next">▶</button>
          </div>
        </div>
        
        <div class="control-group">
          <h3>縮尺合わせ</h3>
          <button id="scale-match-mode">対角2点で縮尺合わせ</button>
          <button id="scale-match-reset">ポイントリセット</button>
          <div class="scale-match-info" id="scale-match-info">
            <div class="scale-match-status" id="scale-match-status">ベース図面の1点目を選択してください</div>
          </div>
        </div>
        
        <div class="control-group">
          <h3>トリミング</h3>
          <button id="crop-mode">トリミングモード</button>
          <div class="crop-info" id="crop-info" style="display: none;"><div>範囲選択して適用を押してください</div></div>
          <button id="apply-crop">トリミング適用</button>
          <button id="reset-crop">トリミングリセット</button>
        </div>
        
        <div class="control-group">
          <h3>透過度</h3>
          <div class="slider-container">
            <input type="range" id="opacity" class="slider" min="0" max="100" value="50">
            <input type="number" id="opacity-input" class="scale-input" min="0" max="100" value="50">
            <span class="scale-unit">%</span>
          </div>
        </div>
        
        <div class="control-group">
          <h3>色変更</h3>
          <label style="display: block; margin-bottom: 10px;">
            <input type="checkbox" id="color-mode"> 単色モード
          </label>
          
          <div id="normal-color-controls">
            <div class="color-controls">
              <label>色調整:</label>
              <input type="color" id="color-filter" class="color-input" value="#ff0000">
            </div>
          </div>
          
          <div id="mono-color-controls" class="mono-options" style="display: none;">
            <div class="color-row">
              <span class="color-label">黒→</span>
              <input type="color" id="black-replace" class="color-input" value="#000000">
              <button id="black-transparent" class="transparent-btn">透明</button>
              <div id="black-transparent-indicator" class="transparent-indicator"></div>
            </div>
            <div class="color-row">
              <span class="color-label">白→</span>
              <input type="color" id="white-replace" class="color-input" value="#ffffff">
              <button id="white-transparent" class="transparent-btn">透明</button>
              <div id="white-transparent-indicator" class="transparent-indicator"></div>
            </div>
            <div class="threshold-container">
              <label>閾値:</label>
              <div class="slider-container">
                <input type="range" id="threshold" class="slider" min="0" max="255" value="128">
                <input type="number" id="threshold-input" class="scale-input" min="0" max="255" value="128">
              </div>
            </div>
            <label style="display: block; margin-top: 10px;">
              <input type="checkbox" id="invert-mono"> 白黒反転
            </label>
          </div>
          
          <div class="preset-colors">
            <div class="preset-color" style="background: #ff0000" data-color="#ff0000"></div>
            <div class="preset-color" style="background: #00ff00" data-color="#00ff00"></div>
            <div class="preset-color" style="background: #0000ff" data-color="#0000ff"></div>
            <div class="preset-color" style="background: #ffff00" data-color="#ffff00"></div>
            <div class="preset-color" style="background: #ff00ff" data-color="#ff00ff"></div>
            <div class="preset-color" style="background: #00ffff" data-color="#00ffff"></div>
          </div>
          
          <button id="apply-color">色を適用</button>
          <button id="reset-color">色をリセット</button>
        </div>
        
        <div class="control-group">
          <h3>拡大縮小</h3>
          <div class="slider-container">
            <input type="range" id="scale" class="slider" min="50" max="500" value="100">
            <input type="number" id="scale-input" class="scale-input" min="50" max="500" value="100">
            <span class="scale-unit">%</span>
          </div>
          <div class="slider-container">
            <label style="width: 80px;">重ねる側:</label>
            <input type="range" id="overlay-scale" class="slider" min="10" max="500" value="100">
            <input type="number" id="overlay-scale-input" class="scale-input" min="10" max="500" value="100">
            <span class="scale-unit">%</span>
          </div>
        </div>
        
        <div class="control-group">
          <h3>位置リセット</h3>
          <button id="reset-position">中央に配置</button>
          <button id="fit-to-base">ベースに合わせる</button>
        </div>
        
        <div class="control-group">
          <h3>ブレンドモード</h3>
          <select id="blend-mode" style="width: 100%; padding: 5px;">
            <option value="normal">通常</option>
            <option value="multiply">乗算</option>
            <option value="screen">スクリーン</option>
            <option value="overlay">オーバーレイ</option>
            <option value="difference">差分</option>
            <option value="exclusion">除外</option>
            <option value="darken">暗く</option>
            <option value="lighten">明るく</option>
          </select>
        </div>
        
        <div class="control-group">
          <h3>エクスポート</h3>
          <select id="pdf-size" class="pdf-size-select">
            <option value="a3" selected>A3サイズ</option>
            <option value="a4">A4サイズ</option>
            <option value="a2">A2サイズ</option>
            <option value="a1">A1サイズ</option>
          </select>
          <button id="export-jpg" class="export-btn">JPGで保存</button>
          <button id="export-pdf" class="export-btn">PDFで保存</button>
          <button id="export-png" class="export-btn">PNGで保存</button>
          <div class="quality-info">※最高画質出力(A3・300dpi相当)</div>
        </div>
      </div>
      
      <div class="main-area">
        <div id="canvas-container">
          <canvas id="base-canvas" class="pdf-canvas"></canvas>
          <div id="overlay-wrapper">
            <canvas id="overlay-canvas"></canvas>
            <div class="resize-handle nw"></div>
            <div class="resize-handle ne"></div>
            <div class="resize-handle sw"></div>
            <div class="resize-handle se"></div>
          </div>
          <canvas id="crop-overlay" class="pdf-canvas"></canvas>
        </div>
      </div>
    </div>
    
    <script>
    pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
    
    function showToast(message, duration = 2000) {
      const toast = document.getElementById('toast');
      toast.textContent = message;
      toast.classList.add('show');
      setTimeout(() => toast.classList.remove('show'), duration);
    }
    
    const DB_NAME = 'pdfOverlayDB';
    const DB_VERSION = 1;
    const STORE_NAME = 'slots';
    let db = null;
    
    async function initDB() {
      return new Promise((resolve, reject) => {
        const request = indexedDB.open(DB_NAME, DB_VERSION);
        request.onerror = () => reject(request.error);
        request.onsuccess = () => { db = request.result; resolve(db); };
        request.onupgradeneeded = (e) => {
          const database = e.target.result;
          if (!database.objectStoreNames.contains(STORE_NAME)) database.createObjectStore(STORE_NAME, { keyPath: 'id' });
        };
      });
    }
    
    async function saveSlotToDB(slotNum, data) {
      return new Promise((resolve, reject) => {
        const tx = db.transaction(STORE_NAME, 'readwrite');
        const store = tx.objectStore(STORE_NAME);
        data.id = slotNum;
        store.put(data);
        tx.oncomplete = () => resolve();
        tx.onerror = () => reject(tx.error);
      });
    }
    
    async function getSlotFromDB(slotNum) {
      return new Promise((resolve, reject) => {
        const tx = db.transaction(STORE_NAME, 'readonly');
        const store = tx.objectStore(STORE_NAME);
        const request = store.get(slotNum);
        request.onsuccess = () => resolve(request.result || null);
        request.onerror = () => reject(request.error);
      });
    }
    
    async function deleteSlotFromDB(slotNum) {
      return new Promise((resolve, reject) => {
        const tx = db.transaction(STORE_NAME, 'readwrite');
        const store = tx.objectStore(STORE_NAME);
        store.delete(slotNum);
        tx.oncomplete = () => resolve();
        tx.onerror = () => reject(tx.error);
      });
    }
    
    async function getAllSlots() {
      return new Promise((resolve, reject) => {
        const tx = db.transaction(STORE_NAME, 'readonly');
        const store = tx.objectStore(STORE_NAME);
        const request = store.getAll();
        request.onsuccess = () => {
          const result = {};
          request.result.forEach(slot => { result[slot.id] = slot; });
          resolve(result);
        };
        request.onerror = () => reject(request.error);
      });
    }
    
    const SLOT_COUNT = 5;
    let basePdfArrayBuffer = null;
    let overlayPdfArrayBuffer = null;
    
    function showLoading() { document.getElementById('loading-overlay').classList.remove('hidden'); }
    function hideLoading() { document.getElementById('loading-overlay').classList.add('hidden'); }
    
    async function saveToSlot(slotNum) {
      showLoading();
      try {
        const data = {
          id: slotNum,
          basePdf: basePdfArrayBuffer,
          overlayPdf: overlayPdfArrayBuffer,
          baseCurrentPage: baseCurrentPage,
          overlayCurrentPage: overlayCurrentPage,
          currentScale: currentScale,
          overlayScale: overlayScale,
          overlayX: overlayX,
          overlayY: overlayY,
          relativePosition: relativePosition,
          isPositionSet: isPositionSet,
          opacity: document.getElementById('opacity').value,
          blendMode: document.getElementById('blend-mode').value,
          colorMode: document.getElementById('color-mode').checked,
          colorFilter: document.getElementById('color-filter').value,
          blackReplace: document.getElementById('black-replace').value,
          whiteReplace: document.getElementById('white-replace').value,
          threshold: document.getElementById('threshold').value,
          invertMono: document.getElementById('invert-mono').checked,
          blackTransparent: blackTransparent,
          whiteTransparent: whiteTransparent,
          pdfSize: document.getElementById('pdf-size').value,
          // 色適用状態も保存
          isColorApplied: currentState.isColorApplied,
          savedAt: new Date().toLocaleString('ja-JP')
        };
        await saveSlotToDB(slotNum, data);
        await renderSlotUI();
        showToast(`スロット${slotNum}に保存しました`);
      } finally {
        hideLoading();
      }
    }
    
    async function loadFromSlot(slotNum) {
      showLoading();
      try {
        const slot = await getSlotFromDB(slotNum);
        if (!slot) return;
    
        if (slot.basePdf) {
          basePdfArrayBuffer = slot.basePdf;
          basePdf = await pdfjsLib.getDocument(slot.basePdf.slice(0)).promise;
          baseCurrentPage = slot.baseCurrentPage || 1;
          document.getElementById('base-page').textContent = `${baseCurrentPage}/${basePdf.numPages}`;
        }
    
        if (slot.overlayPdf) {
          overlayPdfArrayBuffer = slot.overlayPdf;
          overlayPdf = await pdfjsLib.getDocument(slot.overlayPdf.slice(0)).promise;
          overlayCurrentPage = slot.overlayCurrentPage || 1;
          document.getElementById('overlay-page').textContent = `${overlayCurrentPage}/${overlayPdf.numPages}`;
        }
    
        currentScale = slot.currentScale;
        overlayScale = slot.overlayScale;
        overlayX = slot.overlayX;
        overlayY = slot.overlayY;
        relativePosition = slot.relativePosition;
        isPositionSet = slot.isPositionSet;
        blackTransparent = slot.blackTransparent;
        whiteTransparent = slot.whiteTransparent;
    
        // UI更新
        document.getElementById('scale').value = currentScale * 100;
        document.getElementById('scale-input').value = currentScale * 100;
        document.getElementById('overlay-scale').value = overlayScale * 100;
        document.getElementById('overlay-scale-input').value = overlayScale * 100;
        document.getElementById('opacity').value = slot.opacity;
        document.getElementById('opacity-input').value = slot.opacity;
        document.getElementById('blend-mode').value = slot.blendMode;
        document.getElementById('color-mode').checked = slot.colorMode;
        document.getElementById('color-filter').value = slot.colorFilter;
        document.getElementById('black-replace').value = slot.blackReplace;
        document.getElementById('white-replace').value = slot.whiteReplace;
        document.getElementById('threshold').value = slot.threshold;
        document.getElementById('threshold-input').value = slot.threshold;
        document.getElementById('invert-mono').checked = slot.invertMono;
        document.getElementById('pdf-size').value = slot.pdfSize;
    
        overlayCanvas.style.opacity = slot.opacity / 100;
        overlayCanvas.style.mixBlendMode = slot.blendMode;
    
        // 単色モード表示切替
        document.getElementById('normal-color-controls').style.display = slot.colorMode ? 'none' : 'block';
        document.getElementById('mono-color-controls').style.display = slot.colorMode ? 'block' : 'none';
    
        // 透明ボタン状態
        document.getElementById('black-transparent').classList.toggle('active', blackTransparent);
        document.getElementById('white-transparent').classList.toggle('active', whiteTransparent);
        document.getElementById('black-replace').style.display = blackTransparent ? 'none' : 'inline-block';
        document.getElementById('white-replace').style.display = whiteTransparent ? 'none' : 'inline-block';
        document.getElementById('black-transparent-indicator').style.display = blackTransparent ? 'inline-block' : 'none';
        document.getElementById('white-transparent-indicator').style.display = whiteTransparent ? 'inline-block' : 'none';
    
        // レンダリング
        if (basePdf) await renderBasePDF();
        if (overlayPdf) {
          await renderOverlayPDF(false);
          overlayWrapper.style.left = overlayX + 'px';
          overlayWrapper.style.top = overlayY + 'px';
          
          // 色適用状態を復元
          if (slot.isColorApplied) {
            currentState.isColorApplied = true;
            applyColorToCanvas(false);
          }
        }
      } finally {
        hideLoading();
      }
    }
    
    async function deleteSlot(slotNum, e) {
      e.stopPropagation();
      if (!confirm(`スロット${slotNum}を削除?`)) return;
      await deleteSlotFromDB(slotNum);
      await renderSlotUI();
    }
    
    async function renderSlotUI() {
      const container = document.getElementById('slotContainer');
      const slots = await getAllSlots();
      container.innerHTML = '';
      for (let i = 1; i <= SLOT_COUNT; i++) {
        const slot = slots[i];
        const group = document.createElement('div');
        group.className = 'slot-group';
        const saveBtn = document.createElement('button');
        saveBtn.className = 'slot-btn save';
        saveBtn.textContent = `S${i}`;
        saveBtn.title = slot ? `上書き保存\n${slot.savedAt}` : `スロット${i}に保存`;
        saveBtn.onclick = () => saveToSlot(i);
        const loadBtn = document.createElement('button');
        loadBtn.className = 'slot-btn load';
        loadBtn.textContent = `L${i}`;
        loadBtn.title = slot ? `読込\n${slot.savedAt}` : '(空)';
        loadBtn.onclick = () => loadFromSlot(i);
        loadBtn.disabled = !slot;
        group.appendChild(saveBtn);
        group.appendChild(loadBtn);
        if (slot) {
          const delBtn = document.createElement('button');
          delBtn.className = 'slot-btn delete';
          delBtn.textContent = '×';
          delBtn.title = '削除';
          delBtn.onclick = (e) => deleteSlot(i, e);
          group.appendChild(delBtn);
        }
        container.appendChild(group);
      }
    }
    
    let basePdf = null, overlayPdf = null;
    let baseCurrentPage = 1, overlayCurrentPage = 1;
    let currentScale = 1, overlayScale = 1;
    let overlayX = 0, overlayY = 0;
    let relativePosition = { x: 0.5, y: 0.5 };
    let isPositionSet = false;
    let scaleMatchMode = false;
    let scaleMatchPoints = { base1: null, base2: null, overlay1: null, overlay2: null };
    let currentScaleMatchStep = 0;
    let currentState = { cropData: null, colorSettings: null, isCropped: false, isColorApplied: false };
    let blackTransparent = false, whiteTransparent = false;
    let cropMode = false;
    let cropStartX = 0, cropStartY = 0, cropEndX = 0, cropEndY = 0;
    let cropRect = null, isDrawingCrop = false;
    
    const baseCanvas = document.getElementById('base-canvas');
    const overlayCanvas = document.getElementById('overlay-canvas');
    const overlayWrapper = document.getElementById('overlay-wrapper');
    const cropOverlay = document.getElementById('crop-overlay');
    const baseCtx = baseCanvas.getContext('2d');
    const overlayCtx = overlayCanvas.getContext('2d', { alpha: true });
    const cropCtx = cropOverlay.getContext('2d');
    
    overlayCtx.imageSmoothingEnabled = true;
    overlayCtx.imageSmoothingQuality = 'high';
    baseCtx.imageSmoothingEnabled = true;
    baseCtx.imageSmoothingQuality = 'high';
    
    let overlayImageData = null, originalOverlayImageData = null, fullOriginalImageData = null;
    let isResizing = false, resizeHandle = null;
    let originalWidth = 0, originalHeight = 0, resizeStartX = 0, resizeStartY = 0;
    
    function saveRelativePosition() {
      if (baseCanvas.width > 0 && baseCanvas.height > 0) {
        const centerX = overlayX + (overlayCanvas.width / 2);
        const centerY = overlayY + (overlayCanvas.height / 2);
        relativePosition.x = centerX / baseCanvas.width;
        relativePosition.y = centerY / baseCanvas.height;
        isPositionSet = true;
      }
    }
    
    function restorePositionFromRelative() {
      if (!isPositionSet) { centerOverlay(); return; }
      const centerX = relativePosition.x * baseCanvas.width;
      const centerY = relativePosition.y * baseCanvas.height;
      overlayX = centerX - (overlayCanvas.width / 2);
      overlayY = centerY - (overlayCanvas.height / 2);
      overlayWrapper.style.left = overlayX + 'px';
      overlayWrapper.style.top = overlayY + 'px';
    }
    
    document.getElementById('scale-match-mode').addEventListener('click', function() {
      scaleMatchMode = !scaleMatchMode;
      cropMode = false;
      document.getElementById('crop-mode').classList.remove('active');
      this.classList.toggle('active');
      document.getElementById('canvas-container').classList.toggle('scale-match-active');
      document.getElementById('scale-match-info').classList.toggle('active');
      if (scaleMatchMode) { currentScaleMatchStep = 0; updateScaleMatchStatus(); }
      else { clearScaleMatchPoints(); }
    });
    
    document.getElementById('scale-match-reset').addEventListener('click', () => {
      clearScaleMatchPoints();
      currentScaleMatchStep = 0;
      if (scaleMatchMode) updateScaleMatchStatus();
    });
    
    function clearScaleMatchPoints() {
      scaleMatchPoints = { base1: null, base2: null, overlay1: null, overlay2: null };
      document.querySelectorAll('.point-marker').forEach(m => m.remove());
    }
    
    function updateScaleMatchStatus() {
      const statusEl = document.getElementById('scale-match-status');
      const messages = ['ベース図面の1点目を選択','ベース図面の2点目を選択','重ねる図面の1点目を選択','重ねる図面の2点目を選択'];
      if (currentScaleMatchStep < messages.length) statusEl.textContent = messages[currentScaleMatchStep];
      else { statusEl.textContent = '縮尺を計算中...'; calculateAndApplyScale(); }
    }
    
    function addPointMarker(x, y, className) {
      const marker = document.createElement('div');
      marker.className = `point-marker ${className}`;
      marker.style.left = (x - 5) + 'px';
      marker.style.top = (y - 5) + 'px';
      document.getElementById('canvas-container').appendChild(marker);
    }
    
    baseCanvas.addEventListener('click', (e) => {
      if (!scaleMatchMode || currentScaleMatchStep >= 2) return;
      const rect = baseCanvas.getBoundingClientRect();
      const containerRect = document.getElementById('canvas-container').getBoundingClientRect();
      const x = e.clientX - containerRect.left, y = e.clientY - containerRect.top;
      if (currentScaleMatchStep === 0) { scaleMatchPoints.base1 = { x: e.clientX - rect.left, y: e.clientY - rect.top }; addPointMarker(x, y, 'base-1'); }
      else if (currentScaleMatchStep === 1) { scaleMatchPoints.base2 = { x: e.clientX - rect.left, y: e.clientY - rect.top }; addPointMarker(x, y, 'base-2'); }
      currentScaleMatchStep++;
      updateScaleMatchStatus();
    });
    
    overlayCanvas.addEventListener('click', (e) => {
      if (!scaleMatchMode || currentScaleMatchStep < 2 || currentScaleMatchStep >= 4) return;
      const rect = overlayCanvas.getBoundingClientRect();
      const containerRect = document.getElementById('canvas-container').getBoundingClientRect();
      const x = e.clientX - containerRect.left, y = e.clientY - containerRect.top;
      if (currentScaleMatchStep === 2) { scaleMatchPoints.overlay1 = { x: e.clientX - rect.left, y: e.clientY - rect.top }; addPointMarker(x, y, 'overlay-1'); }
      else if (currentScaleMatchStep === 3) { scaleMatchPoints.overlay2 = { x: e.clientX - rect.left, y: e.clientY - rect.top }; addPointMarker(x, y, 'overlay-2'); }
      currentScaleMatchStep++;
      updateScaleMatchStatus();
    });
    
    async function calculateAndApplyScale() {
      const baseDistance = Math.sqrt(Math.pow(scaleMatchPoints.base2.x - scaleMatchPoints.base1.x, 2) + Math.pow(scaleMatchPoints.base2.y - scaleMatchPoints.base1.y, 2));
      const overlayDistance = Math.sqrt(Math.pow(scaleMatchPoints.overlay2.x - scaleMatchPoints.overlay1.x, 2) + Math.pow(scaleMatchPoints.overlay2.y - scaleMatchPoints.overlay1.y, 2));
      const newScale = (baseDistance / overlayDistance) * overlayScale;
      const scalePercent = Math.round(newScale * 100);
      document.getElementById('overlay-scale').value = Math.min(500, Math.max(10, scalePercent));
      document.getElementById('overlay-scale-input').value = scalePercent;
      overlayScale = newScale;
      await renderOverlayPDF(true);
      const scaledOverlayX = scaleMatchPoints.overlay1.x * (newScale / overlayScale);
      const scaledOverlayY = scaleMatchPoints.overlay1.y * (newScale / overlayScale);
      overlayX = scaleMatchPoints.base1.x - scaledOverlayX;
      overlayY = scaleMatchPoints.base1.y - scaledOverlayY;
      overlayWrapper.style.left = overlayX + 'px';
      overlayWrapper.style.top = overlayY + 'px';
      saveRelativePosition();
      scaleMatchMode = false;
      document.getElementById('scale-match-mode').classList.remove('active');
      document.getElementById('canvas-container').classList.remove('scale-match-active');
      document.getElementById('scale-match-info').classList.remove('active');
      clearScaleMatchPoints();
      document.getElementById('scale-match-status').textContent = '縮尺合わせ完了';
    }
    
    function setupSliderInputSync(sliderId, inputId, callback) {
      const slider = document.getElementById(sliderId);
      const input = document.getElementById(inputId);
      slider.addEventListener('input', (e) => { input.value = e.target.value; callback(e.target.value); });
      input.addEventListener('input', (e) => {
        let value = parseInt(e.target.value);
        if (isNaN(value)) return;
        value = Math.max(parseInt(input.min), Math.min(parseInt(input.max), value));
        e.target.value = value; slider.value = value; callback(value);
      });
      input.addEventListener('blur', (e) => { if (e.target.value === '') e.target.value = slider.value; });
    }
    
    setupSliderInputSync('opacity', 'opacity-input', (value) => { overlayCanvas.style.opacity = value / 100; });
    setupSliderInputSync('threshold', 'threshold-input', () => {});
    setupSliderInputSync('scale', 'scale-input', async (value) => { saveRelativePosition(); currentScale = value / 100; await renderBasePDF(); await renderOverlayPDF(true); restorePositionFromRelative(); });
    setupSliderInputSync('overlay-scale', 'overlay-scale-input', async (value) => { saveRelativePosition(); overlayScale = value / 100; await renderOverlayPDF(true); restorePositionFromRelative(); });
    
    document.querySelectorAll('.resize-handle').forEach(handle => { handle.addEventListener('mousedown', startResize); });
    
    function startResize(e) {
      isResizing = true; resizeHandle = e.target.classList[1];
      resizeStartX = e.clientX; resizeStartY = e.clientY;
      originalWidth = overlayCanvas.width; originalHeight = overlayCanvas.height;
      document.addEventListener('mousemove', doResize);
      document.addEventListener('mouseup', stopResize);
      e.preventDefault(); e.stopPropagation();
    }
    
    async function doResize(e) {
      if (!isResizing) return;
      const deltaX = e.clientX - resizeStartX;
      let newWidth = originalWidth;
      const aspectRatio = originalWidth / originalHeight;
      const centerX = overlayX + originalWidth / 2, centerY = overlayY + originalHeight / 2;
      switch(resizeHandle) {
        case 'se': newWidth = originalWidth + deltaX; break;
        case 'sw': newWidth = originalWidth - deltaX; overlayX = centerX - newWidth / 2; break;
        case 'ne': newWidth = originalWidth + deltaX; overlayY = centerY - (newWidth / aspectRatio) / 2; break;
        case 'nw': newWidth = originalWidth - deltaX; overlayX = centerX - newWidth / 2; overlayY = centerY - (newWidth / aspectRatio) / 2; break;
      }
      if (newWidth < 50) return;
      const baseWidth = currentState.isCropped ? currentState.cropData.width : (fullOriginalImageData ? fullOriginalImageData.width / (currentScale * 2) : overlayCanvas.width);
      const newScale = (newWidth / (baseWidth * currentScale)) * 100;
      document.getElementById('overlay-scale').value = Math.min(500, Math.max(10, newScale));
      document.getElementById('overlay-scale-input').value = Math.round(newScale);
      saveRelativePosition(); overlayScale = newScale / 100; await renderOverlayPDF(true);
      overlayWrapper.style.left = overlayX + 'px'; overlayWrapper.style.top = overlayY + 'px';
    }
    
    async function stopResize() { isResizing = false; resizeHandle = null; saveRelativePosition(); document.removeEventListener('mousemove', doResize); document.removeEventListener('mouseup', stopResize); }
    
    async function renderBasePDF() {
      if (!basePdf) return;
      const page = await basePdf.getPage(baseCurrentPage);
      const viewport = page.getViewport({ scale: currentScale });
      baseCanvas.width = viewport.width; baseCanvas.height = viewport.height;
      await page.render({ canvasContext: baseCtx, viewport: viewport, intent: 'display' }).promise;
      updateContainerSize();
    }
    
    async function renderOverlayPDF(preserveState = false) {
      if (!overlayPdf) return;
      const page = await overlayPdf.getPage(overlayCurrentPage);
      const renderScale = 2;
      const viewport = page.getViewport({ scale: currentScale * overlayScale * renderScale });
      const tempCanvas = document.createElement('canvas');
      tempCanvas.width = viewport.width; tempCanvas.height = viewport.height;
      const tempCtx = tempCanvas.getContext('2d');
      tempCtx.imageSmoothingEnabled = true; tempCtx.imageSmoothingQuality = 'high';
      await page.render({ canvasContext: tempCtx, viewport: viewport, intent: 'display' }).promise;
      const actualWidth = viewport.width / renderScale, actualHeight = viewport.height / renderScale;
      fullOriginalImageData = tempCtx.getImageData(0, 0, tempCanvas.width, tempCanvas.height);
      if (preserveState && currentState.isCropped && currentState.cropData) {
        const scale = currentScale * overlayScale * renderScale;
        const cropX = currentState.cropData.x * scale, cropY = currentState.cropData.y * scale;
        const cropWidth = currentState.cropData.width * scale, cropHeight = currentState.cropData.height * scale;
        overlayCanvas.width = cropWidth / renderScale; overlayCanvas.height = cropHeight / renderScale;
        overlayWrapper.style.width = (cropWidth / renderScale) + 'px'; overlayWrapper.style.height = (cropHeight / renderScale) + 'px';
        overlayCtx.drawImage(tempCanvas, cropX, cropY, cropWidth, cropHeight, 0, 0, cropWidth / renderScale, cropHeight / renderScale);
      } else {
        overlayCanvas.width = actualWidth; overlayCanvas.height = actualHeight;
        overlayWrapper.style.width = actualWidth + 'px'; overlayWrapper.style.height = actualHeight + 'px';
        overlayCtx.drawImage(tempCanvas, 0, 0, actualWidth, actualHeight);
      }
      overlayImageData = overlayCtx.getImageData(0, 0, overlayCanvas.width, overlayCanvas.height);
      originalOverlayImageData = overlayCtx.getImageData(0, 0, overlayCanvas.width, overlayCanvas.height);
      if (preserveState && currentState.isColorApplied) applyColorToCanvas(false);
    }
    
    function updateContainerSize() {
      const container = document.getElementById('canvas-container');
      container.style.width = baseCanvas.width + 'px'; container.style.height = baseCanvas.height + 'px';
      cropOverlay.width = baseCanvas.width; cropOverlay.height = baseCanvas.height;
    }
    
    function centerOverlay() {
      overlayX = (baseCanvas.width - overlayCanvas.width) / 2;
      overlayY = (baseCanvas.height - overlayCanvas.height) / 2;
      overlayWrapper.style.left = overlayX + 'px'; overlayWrapper.style.top = overlayY + 'px';
      saveRelativePosition();
    }
    
    document.getElementById('crop-mode').addEventListener('click', function() {
      cropMode = !cropMode; scaleMatchMode = false;
      document.getElementById('scale-match-mode').classList.remove('active');
      this.classList.toggle('active');
      document.getElementById('canvas-container').classList.toggle('crop-active');
      document.getElementById('canvas-container').classList.remove('scale-match-active');
      document.getElementById('crop-info').style.display = cropMode ? 'block' : 'none';
      document.getElementById('scale-match-info').classList.remove('active');
      if (!cropMode) clearCropOverlay();
    });
    
    overlayCanvas.addEventListener('mousedown', (e) => {
      if (scaleMatchMode) return;
      if (!cropMode) { startDrag(e); return; }
      const rect = overlayCanvas.getBoundingClientRect();
      cropStartX = e.clientX - rect.left; cropStartY = e.clientY - rect.top;
      isDrawingCrop = true;
      document.addEventListener('mousemove', drawCropSelection);
      document.addEventListener('mouseup', endCropSelection);
    });
    
    function drawCropSelection(e) {
      if (!isDrawingCrop) return;
      const rect = overlayCanvas.getBoundingClientRect();
      cropEndX = e.clientX - rect.left; cropEndY = e.clientY - rect.top;
      cropCtx.clearRect(0, 0, cropOverlay.width, cropOverlay.height);
      const x = Math.min(cropStartX, cropEndX) + overlayX, y = Math.min(cropStartY, cropEndY) + overlayY;
      const width = Math.abs(cropEndX - cropStartX), height = Math.abs(cropEndY - cropStartY);
      cropCtx.strokeStyle = '#ff0000'; cropCtx.lineWidth = 2; cropCtx.setLineDash([5, 5]);
      cropCtx.strokeRect(x, y, width, height);
      cropRect = { x: Math.min(cropStartX, cropEndX), y: Math.min(cropStartY, cropEndY), width, height };
    }
    
    function endCropSelection() { if (!isDrawingCrop) return; document.removeEventListener('mousemove', drawCropSelection); document.removeEventListener('mouseup', endCropSelection); isDrawingCrop = false; }
    
    document.getElementById('apply-crop').addEventListener('click', () => {
      if (!cropRect || !overlayImageData) return;
      saveRelativePosition();
      currentState.cropData = { x: cropRect.x / (currentScale * overlayScale), y: cropRect.y / (currentScale * overlayScale), width: cropRect.width / (currentScale * overlayScale), height: cropRect.height / (currentScale * overlayScale) };
      currentState.isCropped = true;
      const tempCanvas = document.createElement('canvas');
      tempCanvas.width = cropRect.width; tempCanvas.height = cropRect.height;
      const tempCtx = tempCanvas.getContext('2d');
      tempCtx.drawImage(overlayCanvas, cropRect.x, cropRect.y, cropRect.width, cropRect.height, 0, 0, cropRect.width, cropRect.height);
      overlayCanvas.width = cropRect.width; overlayCanvas.height = cropRect.height;
      overlayWrapper.style.width = cropRect.width + 'px'; overlayWrapper.style.height = cropRect.height + 'px';
      overlayCtx.drawImage(tempCanvas, 0, 0);
      overlayImageData = overlayCtx.getImageData(0, 0, overlayCanvas.width, overlayCanvas.height);
      originalOverlayImageData = overlayCtx.getImageData(0, 0, overlayCanvas.width, overlayCanvas.height);
      if (currentState.isColorApplied) applyColorToCanvas(false);
      restorePositionFromRelative();
      clearCropOverlay(); cropMode = false; cropRect = null;
      document.getElementById('crop-mode').classList.remove('active');
      document.getElementById('canvas-container').classList.remove('crop-active');
      document.getElementById('crop-info').style.display = 'none';
    });
    
    document.getElementById('reset-crop').addEventListener('click', async () => {
      cropRect = null; currentState.cropData = null; currentState.isCropped = false;
      clearCropOverlay(); saveRelativePosition();
      if (overlayPdf) { await renderOverlayPDF(false); if (currentState.isColorApplied) applyColorToCanvas(false); restorePositionFromRelative(); }
    });
    
    function clearCropOverlay() { cropCtx.clearRect(0, 0, cropOverlay.width, cropOverlay.height); }
    
    document.getElementById('base-file').addEventListener('change', async (e) => {
      const file = e.target.files[0]; if (!file) return;
      basePdfArrayBuffer = await file.arrayBuffer();
      basePdf = await pdfjsLib.getDocument(basePdfArrayBuffer.slice(0)).promise;
      baseCurrentPage = 1;
      document.getElementById('base-page').textContent = `${baseCurrentPage}/${basePdf.numPages}`;
      await renderBasePDF();
      if (overlayPdf) restorePositionFromRelative();
    });
    
    document.getElementById('overlay-file').addEventListener('change', async (e) => {
      const file = e.target.files[0]; if (!file) return;
      overlayPdfArrayBuffer = await file.arrayBuffer();
      overlayPdf = await pdfjsLib.getDocument(overlayPdfArrayBuffer.slice(0)).promise;
      overlayCurrentPage = 1;
      currentState = { cropData: null, colorSettings: null, isCropped: false, isColorApplied: false };
      cropRect = null; isPositionSet = false;
      document.getElementById('overlay-page').textContent = `${overlayCurrentPage}/${overlayPdf.numPages}`;
      await renderOverlayPDF(false); centerOverlay();
    });
    
    document.getElementById('base-prev').addEventListener('click', async () => { if (!basePdf || baseCurrentPage <= 1) return; baseCurrentPage--; document.getElementById('base-page').textContent = `${baseCurrentPage}/${basePdf.numPages}`; await renderBasePDF(); if (overlayPdf) restorePositionFromRelative(); });
    document.getElementById('base-next').addEventListener('click', async () => { if (!basePdf || baseCurrentPage >= basePdf.numPages) return; baseCurrentPage++; document.getElementById('base-page').textContent = `${baseCurrentPage}/${basePdf.numPages}`; await renderBasePDF(); if (overlayPdf) restorePositionFromRelative(); });
    document.getElementById('overlay-prev').addEventListener('click', async () => { if (!overlayPdf || overlayCurrentPage <= 1) return; overlayCurrentPage--; currentState = { cropData: null, colorSettings: null, isCropped: false, isColorApplied: false }; cropRect = null; document.getElementById('overlay-page').textContent = `${overlayCurrentPage}/${overlayPdf.numPages}`; await renderOverlayPDF(false); restorePositionFromRelative(); });
    document.getElementById('overlay-next').addEventListener('click', async () => { if (!overlayPdf || overlayCurrentPage >= overlayPdf.numPages) return; overlayCurrentPage++; currentState = { cropData: null, colorSettings: null, isCropped: false, isColorApplied: false }; cropRect = null; document.getElementById('overlay-page').textContent = `${overlayCurrentPage}/${overlayPdf.numPages}`; await renderOverlayPDF(false); restorePositionFromRelative(); });
    
    document.getElementById('color-mode').addEventListener('change', (e) => {
      const isMonoMode = e.target.checked;
      document.getElementById('normal-color-controls').style.display = isMonoMode ? 'none' : 'block';
      document.getElementById('mono-color-controls').style.display = isMonoMode ? 'block' : 'none';
    });
    
    document.getElementById('black-transparent').addEventListener('click', function() {
      blackTransparent = !blackTransparent; this.classList.toggle('active');
      document.getElementById('black-replace').style.display = blackTransparent ? 'none' : 'inline-block';
      document.getElementById('black-transparent-indicator').style.display = blackTransparent ? 'inline-block' : 'none';
      if (document.getElementById('color-mode').checked) applyColorToCanvas(true);
    });
    
    document.getElementById('white-transparent').addEventListener('click', function() {
      whiteTransparent = !whiteTransparent; this.classList.toggle('active');
      document.getElementById('white-replace').style.display = whiteTransparent ? 'none' : 'inline-block';
      document.getElementById('white-transparent-indicator').style.display = whiteTransparent ? 'inline-block' : 'none';
      if (document.getElementById('color-mode').checked) applyColorToCanvas(true);
    });
    
    function applyColorToCanvas(saveState = true) {
      if (!overlayImageData) return;
      const colorMode = document.getElementById('color-mode').checked;
      if (saveState) {
        currentState.colorSettings = { colorMode, normalColor: document.getElementById('color-filter').value, blackColor: document.getElementById('black-replace').value, whiteColor: document.getElementById('white-replace').value, threshold: document.getElementById('threshold').value, invert: document.getElementById('invert-mono').checked, blackTransparent, whiteTransparent };
        currentState.isColorApplied = true;
      }
      const newImageData = new ImageData(new Uint8ClampedArray(originalOverlayImageData.data), originalOverlayImageData.width, originalOverlayImageData.height);
      if (colorMode) {
        const blackColor = document.getElementById('black-replace').value, whiteColor = document.getElementById('white-replace').value;
        const threshold = parseInt(document.getElementById('threshold').value), invert = document.getElementById('invert-mono').checked;
        const blackR = parseInt(blackColor.substr(1, 2), 16), blackG = parseInt(blackColor.substr(3, 2), 16), blackB = parseInt(blackColor.substr(5, 2), 16);
        const whiteR = parseInt(whiteColor.substr(1, 2), 16), whiteG = parseInt(whiteColor.substr(3, 2), 16), whiteB = parseInt(whiteColor.substr(5, 2), 16);
        for (let i = 0; i < newImageData.data.length; i += 4) {
          if (newImageData.data[i + 3] > 0) {
            const gray = newImageData.data[i] * 0.299 + newImageData.data[i + 1] * 0.587 + newImageData.data[i + 2] * 0.114;
            let isBlack = gray < threshold; if (invert) isBlack = !isBlack;
            if (isBlack) { if (blackTransparent) newImageData.data[i + 3] = 0; else { newImageData.data[i] = blackR; newImageData.data[i + 1] = blackG; newImageData.data[i + 2] = blackB; } }
            else { if (whiteTransparent) newImageData.data[i + 3] = 0; else { newImageData.data[i] = whiteR; newImageData.data[i + 1] = whiteG; newImageData.data[i + 2] = whiteB; } }
          }
        }
      } else {
        const color = document.getElementById('color-filter').value;
        const r = parseInt(color.substr(1, 2), 16), g = parseInt(color.substr(3, 2), 16), b = parseInt(color.substr(5, 2), 16);
        for (let i = 0; i < newImageData.data.length; i += 4) {
          if (newImageData.data[i + 3] > 0) {
            newImageData.data[i] = Math.min(255, newImageData.data[i] * (r / 128));
            newImageData.data[i + 1] = Math.min(255, newImageData.data[i + 1] * (g / 128));
            newImageData.data[i + 2] = Math.min(255, newImageData.data[i + 2] * (b / 128));
          }
        }
      }
      overlayCtx.putImageData(newImageData, 0, 0); overlayImageData = newImageData;
    }
    
    document.getElementById('apply-color').addEventListener('click', () => applyColorToCanvas(true));
    
    document.getElementById('reset-color').addEventListener('click', () => {
      if (originalOverlayImageData) { overlayCtx.putImageData(originalOverlayImageData, 0, 0); overlayImageData = originalOverlayImageData; }
      currentState.colorSettings = null; currentState.isColorApplied = false;
      blackTransparent = false; whiteTransparent = false;
      document.getElementById('black-transparent').classList.remove('active');
      document.getElementById('white-transparent').classList.remove('active');
      document.getElementById('black-replace').style.display = 'inline-block';
      document.getElementById('white-replace').style.display = 'inline-block';
      document.getElementById('black-transparent-indicator').style.display = 'none';
      document.getElementById('white-transparent-indicator').style.display = 'none';
    });
    
    document.querySelectorAll('.preset-color').forEach(preset => {
      preset.addEventListener('click', () => {
        const color = preset.dataset.color;
        if (document.getElementById('color-mode').checked) { if (!blackTransparent) document.getElementById('black-replace').value = color; }
        else { document.getElementById('color-filter').value = color; }
        applyColorToCanvas(true);
      });
    });
    
    ['black-replace', 'white-replace', 'invert-mono'].forEach(id => { document.getElementById(id).addEventListener('change', () => { if (document.getElementById('color-mode').checked) applyColorToCanvas(true); }); });
    document.getElementById('threshold').addEventListener('input', () => { if (document.getElementById('color-mode').checked) applyColorToCanvas(true); });
    
    let isDragging = false, dragStartX, dragStartY, initialLeft, initialTop;
    
    function startDrag(e) {
      if (cropMode || isResizing || scaleMatchMode) return;
      isDragging = true; dragStartX = e.clientX; dragStartY = e.clientY;
      const rect = overlayWrapper.getBoundingClientRect();
      const containerRect = document.getElementById('canvas-container').getBoundingClientRect();
      initialLeft = rect.left - containerRect.left; initialTop = rect.top - containerRect.top;
      document.addEventListener('mousemove', drag); document.addEventListener('mouseup', stopDrag);
      e.preventDefault();
    }
    
    function drag(e) { if (!isDragging) return; overlayX = initialLeft + (e.clientX - dragStartX); overlayY = initialTop + (e.clientY - dragStartY); overlayWrapper.style.left = overlayX + 'px'; overlayWrapper.style.top = overlayY + 'px'; }
    function stopDrag() { isDragging = false; saveRelativePosition(); document.removeEventListener('mousemove', drag); document.removeEventListener('mouseup', stopDrag); }
    
    document.getElementById('reset-position').addEventListener('click', () => centerOverlay());
    document.getElementById('fit-to-base').addEventListener('click', () => { overlayX = 0; overlayY = 0; overlayWrapper.style.left = '0px'; overlayWrapper.style.top = '0px'; saveRelativePosition(); });
    document.getElementById('blend-mode').addEventListener('change', (e) => { overlayCanvas.style.mixBlendMode = e.target.value; });
    
    function exportAsImage(format) {
      const exportScale = 5;
      const exportCanvas = document.createElement('canvas');
      exportCanvas.width = baseCanvas.width * exportScale; exportCanvas.height = baseCanvas.height * exportScale;
      const exportCtx = exportCanvas.getContext('2d');
      exportCtx.imageSmoothingEnabled = true; exportCtx.imageSmoothingQuality = 'high';
      if (format === 'jpeg') { exportCtx.fillStyle = 'white'; exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height); }
      exportCtx.scale(exportScale, exportScale);
      exportCtx.drawImage(baseCanvas, 0, 0, baseCanvas.width, baseCanvas.height);
      exportCtx.globalAlpha = parseFloat(overlayCanvas.style.opacity || 1);
      exportCtx.globalCompositeOperation = document.getElementById('blend-mode').value;
      exportCtx.drawImage(overlayCanvas, overlayX, overlayY, overlayCanvas.width, overlayCanvas.height);
      exportCanvas.toBlob((blob) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `pdf-kasane-zu_${Date.now()}.${format === 'jpeg' ? 'jpg' : format}`; a.click(); URL.revokeObjectURL(url); }, `image/${format}`, 1.0);
    }
    
    document.getElementById('export-jpg').addEventListener('click', () => exportAsImage('jpeg'));
    document.getElementById('export-png').addEventListener('click', () => exportAsImage('png'));
    
    document.getElementById('export-pdf').addEventListener('click', () => {
      if (!window.jspdf) { alert('PDF出力ライブラリの読み込みに失敗しました。'); return; }
      const { jsPDF } = window.jspdf;
      const pdfSize = document.getElementById('pdf-size').value;
      const exportScale = 4;
      const exportCanvas = document.createElement('canvas');
      exportCanvas.width = baseCanvas.width * exportScale; exportCanvas.height = baseCanvas.height * exportScale;
      const exportCtx = exportCanvas.getContext('2d');
      exportCtx.imageSmoothingEnabled = true; exportCtx.imageSmoothingQuality = 'high';
      exportCtx.scale(exportScale, exportScale);
      exportCtx.fillStyle = 'white'; exportCtx.fillRect(0, 0, exportCanvas.width, exportCanvas.height);
      exportCtx.drawImage(baseCanvas, 0, 0, baseCanvas.width, baseCanvas.height);
      exportCtx.globalAlpha = parseFloat(overlayCanvas.style.opacity || 1);
      exportCtx.globalCompositeOperation = document.getElementById('blend-mode').value;
      exportCtx.drawImage(overlayCanvas, overlayX, overlayY, overlayCanvas.width, overlayCanvas.height);
      const orientation = exportCanvas.width > exportCanvas.height ? 'l' : 'p';
      const pdf = new jsPDF({ orientation, unit: 'mm', format: pdfSize, compress: false });
      const pageWidth = pdf.internal.pageSize.getWidth(), pageHeight = pdf.internal.pageSize.getHeight();
      const imgAspect = exportCanvas.width / exportCanvas.height, pageAspect = pageWidth / pageHeight;
      let imgWidth, imgHeight;
      if (imgAspect > pageAspect) { imgWidth = pageWidth; imgHeight = pageWidth / imgAspect; }
      else { imgHeight = pageHeight; imgWidth = pageHeight * imgAspect; }
      const x = (pageWidth - imgWidth) / 2, y = (pageHeight - imgHeight) / 2;
      pdf.addImage(exportCanvas.toDataURL('image/png', 1.0), 'PNG', x, y, imgWidth, imgHeight, undefined, 'NONE');
      pdf.save(`pdf-kasane-zu_${pdfSize.toUpperCase()}_${Date.now()}.pdf`);
    });
    
    document.getElementById('canvas-container').addEventListener('wheel', async (e) => {
      if (e.ctrlKey) {
        e.preventDefault();
        const delta = e.deltaY > 0 ? -5 : 5;
        let newValue = Math.max(50, Math.min(500, parseInt(document.getElementById('scale').value) + delta));
        document.getElementById('scale').value = newValue; document.getElementById('scale-input').value = newValue;
        saveRelativePosition(); currentScale = newValue / 100;
        await renderBasePDF(); await renderOverlayPDF(true); restorePositionFromRelative();
      }
    });
    
    async function init() { await initDB(); await renderSlotUI(); overlayCanvas.style.opacity = 0.5; }
    init();
    </script>
    
    </body>
    </html>
    
     
     
     
    建築設計として煌めいてます 木造中心に老人ホームやマンションを設計してます 施工管理としてもS造中高層建物の所長(現場責任者)経験 34歳 プログラムは中学時代から AIで時短するのが快感な今日この頃 ◆主要資格 一級建築士 一級施工管理技士 建築積算士 福祉住環境コーデ二級

    あなたへのおすすめ