<!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>
目次
コメント