MENU

PNG画像をJPGに変換(圧縮)するページ(横幅1200px)

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PNG→JPG一括変換(横幅1200px)</title>
<link rel="stylesheet" href="../style.css" />
<style>
  /* --- このページ専用の軽いスタイル(他CSSとぶつからないように接頭辞) --- */
  .cvt-wrap { display: grid; gap: 16px; }
  .cvt-head { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; }
  .cvt-opts { display: flex; flex-wrap: wrap; gap: 12px; align-items: center; }
  .cvt-opts .row{ display:flex; gap:8px; align-items:center; }
  .cvt-drop {
    border: 2px dashed #cbd5e1; border-radius: 12px; padding: 28px;
    text-align: center; background:#f8fafc; transition: 0.2s ease;
  }
  .cvt-drop.dragover { background:#eef2ff; border-color:#6366f1; }
  .cvt-drop p { margin: 6px 0 0; color:#64748b; }
  .cvt-btn { display:inline-flex; align-items:center; gap:.5em; border:1px solid #e2e8f0; padding:.6em .9em; border-radius:10px; background:#fff; cursor:pointer; }
  .cvt-btn:hover { background:#f8fafc; }
  .cvt-list { display:grid; grid-template-columns: repeat(auto-fill, minmax(260px,1fr)); gap:14px; }
  .cvt-card { border:1px solid #e5e7eb; border-radius:12px; overflow:hidden; background:#fff; display:flex; flex-direction:column; }
  .cvt-thumb { width:100%; aspect-ratio: 4/3; object-fit:cover; background:#f1f5f9; }
  .cvt-meta { padding:10px 12px; font-size:.92rem; color:#334155; display:grid; gap:6px; }
  .cvt-actions { display:flex; gap:8px; padding:10px 12px; border-top:1px solid #e5e7eb; }
  .cvt-actions a, .cvt-actions button { flex:1; text-align:center; border:1px solid #e2e8f0; background:#fff; border-radius:10px; padding:.5em .8em; cursor:pointer; }
  .cvt-actions a:hover, .cvt-actions button:hover { background:#f8fafc; }
  .cvt-msg { font-size:.9rem; color:#ef4444; }
  .cvt-note { color:#64748b; font-size:.9rem; }
  .cvt-progress { height:6px; background:#e5e7eb; border-radius:999px; overflow:hidden; }
  .cvt-progress > i { display:block; height:100%; width:0%; background:#60a5fa; transition:width .25s ease; }
  .sr-only { position:absolute; left:-9999px; }
  @media (max-width:560px){ .cvt-head{flex-direction:column; align-items:flex-start;} }
</style>
</head>
<body>
  <div id="header-placeholder"></div>
  <div class="page-wrapper">
    <aside id="sidebar">
      <!-- Sidebar content will be injected here by JS -->
    </aside>
    <div class="content-wrapper">
      <div class="main">

        <!-- ===== アプリ本体(.main内に収めています) ===== -->
        <section class="cvt-wrap" id="png2jpg-app" aria-label="PNGをJPGに変換するツール">
          <div class="cvt-head">
            <h1 style="margin:0;font-size:1.25rem;">PNG→JPG 一括変換(横幅1200px)</h1>
            <div class="cvt-opts">
              <div class="row">
                <label for="targetWidth">横幅</label>
                <input id="targetWidth" type="number" value="1200" min="1" step="1" style="width:90px">
                <span>px</span>
              </div>
              <div class="row">
                <label for="noUpscale">
                  <input id="noUpscale" type="checkbox" checked>
                  小さい画像は拡大しない
                </label>
              </div>
              <div class="row">
                <label for="jpgQuality">品質</label>
                <input id="jpgQuality" type="range" min="0.5" max="1" step="0.05" value="0.6" aria-label="JPG品質">
                <span id="jpgQualityVal">0.60</span>
              </div>
              <div class="row">
                <label for="bgColor">背景色(透過塗り)</label>
                <input id="bgColor" type="color" value="#ffffff" title="PNGの透過部分をこの色で塗ります">
              </div>
              <button class="cvt-btn" id="btnAllDownload" type="button" title="出力済みの全JPGを連続ダウンロード">全部ダウンロード</button>
            </div>
          </div>

          <div id="dropZone" class="cvt-drop" tabindex="0" role="button" aria-label="ここにファイルをドラッグ&ドロップ、またはクリックで選択">
            <strong>ここにPNGファイルをドラッグ&ドロップ</strong>
            <p>またはクリックして選択</p>
            <input id="fileInput" class="sr-only" type="file" accept="image/png" multiple />
          </div>

          <div class="cvt-note">※ 端末内のみで処理します(サーバーへは送信しません)。</div>

          <div class="cvt-progress" aria-hidden="true"><i id="progBar"></i></div>

          <div id="errorArea" class="cvt-msg" role="alert" aria-live="polite"></div>

          <div id="resultList" class="cvt-list" aria-live="polite"></div>
        </section>
        <!-- ===== /アプリ本体 ===== -->

      </div>
    </div>
  </div>

  <script src="../load_header.js"></script>
  <script src="../favorites-data.js"></script>

  <script>
  (function(){
    const dropZone = document.getElementById('dropZone');
    const fileInput = document.getElementById('fileInput');
    const resultList = document.getElementById('resultList');
    const errorArea = document.getElementById('errorArea');
    const progBar = document.getElementById('progBar');

    const targetWidthEl = document.getElementById('targetWidth');
    const noUpscaleEl = document.getElementById('noUpscale');
    const jpgQualityEl = document.getElementById('jpgQuality');
    const jpgQualityValEl = document.getElementById('jpgQualityVal');
    const bgColorEl = document.getElementById('bgColor');
    const btnAllDownload = document.getElementById('btnAllDownload');

    jpgQualityEl.addEventListener('input', () => {
      jpgQualityValEl.textContent = Number(jpgQualityEl.value).toFixed(2);
    });

    // ドロップ領域のイベント
    ['dragenter','dragover'].forEach(ev => {
      dropZone.addEventListener(ev, (e) => {
        e.preventDefault(); e.stopPropagation();
        dropZone.classList.add('dragover');
      });
    });
    ['dragleave','dragend','drop'].forEach(ev => {
      dropZone.addEventListener(ev, (e) => {
        if(ev !== 'drop'){ dropZone.classList.remove('dragover'); }
      });
    });

    dropZone.addEventListener('click', () => fileInput.click());
    dropZone.addEventListener('drop', (e) => {
      e.preventDefault(); e.stopPropagation();
      dropZone.classList.remove('dragover');
      handleFiles(e.dataTransfer.files);
    });
    fileInput.addEventListener('change', (e) => handleFiles(e.target.files));

    btnAllDownload.addEventListener('click', () => {
      // 生成済みリンクを順番にクリックして一括保存
      const links = resultList.querySelectorAll('a[data-dl-link]');
      let i = 0;
      const tick = () => {
        if(i >= links.length) return;
        links[i].click();
        i++;
        setTimeout(tick, 250); // 少し間隔を空けつつ連続ダウンロード
      };
      tick();
    });

    async function handleFiles(fileList){
      errorArea.textContent = '';
      const files = Array.from(fileList).filter(f => f && f.type === 'image/png');
      if(files.length === 0){
        errorArea.textContent = 'PNGファイルを選択してください。';
        return;
      }
      resultList.innerHTML = ''; // 新規バッチでリスト初期化(既存を残したい場合は削除)

      progBar.style.width = '0%';
      let done = 0;

      for(const file of files){
        try {
          const card = await processOne(file);
          resultList.appendChild(card);
        } catch(err){
          console.error(err);
          const p = document.createElement('p');
          p.className = 'cvt-msg';
          p.textContent = `変換失敗: ${file.name}(${err && err.message ? err.message : '不明なエラー'})`;
          resultList.appendChild(p);
        } finally {
          done++;
          progBar.style.width = Math.round(done / files.length * 100) + '%';
        }
      }
    }

    function createCard({blob, filename, width, height, thumbDataUrl}){
      const url = URL.createObjectURL(blob);
      const card = document.createElement('div');
      card.className = 'cvt-card';

      const img = document.createElement('img');
      img.className = 'cvt-thumb';
      img.alt = filename;
      img.src = thumbDataUrl || url;

      const meta = document.createElement('div');
      meta.className = 'cvt-meta';
      meta.innerHTML = `
        <div><strong>${filename}</strong></div>
        <div>${width} × ${height}px / ${Math.round(blob.size/1024)} KB</div>
      `;

      const actions = document.createElement('div');
      actions.className = 'cvt-actions';

      const a = document.createElement('a');
      a.href = url;
      a.download = filename;
      a.setAttribute('data-dl-link','');
      a.textContent = 'ダウンロード';

      const copyBtn = document.createElement('button');
      copyBtn.type = 'button';
      copyBtn.textContent = 'クリップボード';
      copyBtn.addEventListener('click', async () => {
        try{
          await navigator.clipboard.write([
            new ClipboardItem({ [blob.type]: blob })
          ]);
          copyBtn.textContent = 'コピー完了';
          setTimeout(()=>copyBtn.textContent='クリップボード', 1200);
        }catch(e){
          alert('コピーに失敗しました:' + e.message);
        }
      });

      actions.appendChild(a);
      actions.appendChild(copyBtn);

      card.appendChild(img);
      card.appendChild(meta);
      card.appendChild(actions);

      return card;
    }

    function loadImage(src){
      return new Promise((resolve, reject) => {
        const img = new Image();
        img.onload = () => resolve(img);
        img.onerror = () => reject(new Error('画像の読み込みに失敗しました'));
        img.src = src;
      });
    }

    async function processOne(file){
      if(file.type !== 'image/png'){
        throw new Error('PNG以外は変換対象外です');
      }
      const arrayBuffer = await file.arrayBuffer();
      const blobUrl = URL.createObjectURL(new Blob([arrayBuffer], {type: file.type}));
      const img = await loadImage(blobUrl);

      const targetW = Math.max(1, parseInt(targetWidthEl.value || '1200', 10));
      const noUpscale = !!noUpscaleEl.checked;
      const quality = Math.min(1, Math.max(0.5, parseFloat(jpgQualityEl.value || '0.9')));

      // 出力サイズ計算
      let outW = targetW;
      if(noUpscale && img.naturalWidth < targetW){
        outW = img.naturalWidth; // 拡大しない
      }
      const scale = outW / img.naturalWidth;
      const outH = Math.round(img.naturalHeight * scale);

      // 透過塗り + リサイズ描画
      const canvas = document.createElement('canvas');
      canvas.width = outW;
      canvas.height = outH;
      const ctx = canvas.getContext('2d');

      // 背景塗り(PNGの透過部分を指定色で埋める)
      ctx.fillStyle = bgColorEl.value || '#ffffff';
      ctx.fillRect(0, 0, outW, outH);

      // 高品質リサイズのために描画前に設定
      ctx.imageSmoothingEnabled = true;
      ctx.imageSmoothingQuality = 'high';
      ctx.drawImage(img, 0, 0, outW, outH);

      const dataUrl = canvas.toDataURL('image/jpeg', quality);
      const jpgBlob = dataURLtoBlob(dataUrl);
      const baseName = file.name.replace(/\.[^.]+$/, '');
      const outName = `${baseName}_w${outW}.jpg`;

      // 小さめのサムネ生成(カードの軽量表示用)
      const thumb = document.createElement('canvas');
      const tw = 400;
      const ts = tw / outW;
      thumb.width = tw;
      thumb.height = Math.max(1, Math.round(outH * ts));
      const tctx = thumb.getContext('2d');
      tctx.imageSmoothingEnabled = true;
      tctx.imageSmoothingQuality = 'high';
      tctx.drawImage(canvas, 0, 0, thumb.width, thumb.height);
      const thumbUrl = thumb.toDataURL('image/jpeg', 0.7);

      URL.revokeObjectURL(blobUrl);

      return createCard({ blob: jpgBlob, filename: outName, width: outW, height: outH, thumbDataUrl: thumbUrl });
    }

    function dataURLtoBlob(dataurl) {
      const arr = dataurl.split(','), mime = arr[0].match(/:(.*?);/)[1];
      const bstr = atob(arr[1]);
      let n = bstr.length; const u8arr = new Uint8Array(n);
      while (n--) { u8arr[n] = bstr.charCodeAt(n); }
      return new Blob([u8arr], { type: mime });
    }
  })();
  </script>
</body>
</html>
よかったらシェアしてね!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

日本語が含まれない投稿は無視されますのでご注意ください。(スパム対策)

目次