表示速度の改善ため、画像をWebP形式に変換したい。
jpg形式よりも軽量になるということで、一生懸命に変換している方もいらっしゃるのではないでしょうか。
WebPを検索しました。googleさんが作ったようです。
2010年9月30日にGoogleが発表。当初は対応するブラウザが限られていましたが、2020年のiOS 14以降や、その他の主要ブラウザでの対応が進んだことで、現在ではほとんどのユーザーが利用できる状況になっています。
モダンブラウザで表示可能。photoshopで編集もOK。
もうすっかり定着した感じがします。
googleオフィシャル
https://developers.google.com/speed/webp?hl=ja
Squoosh(スクワッシュ)
https://squoosh.app/
最近shopifyではどんな形式でアップしても、自動でwebpで表示させているようです。人気ですね。
軽量なのにキレイ。最高じゃないですか。
Squooshで一生懸命に1枚ずつ軽量化していました。
1枚ずつは大変でないですか。
量が多いとSquooshは大変です。まとめて変換したいです。。
でもそんなことできるのか、さっぱりわかりません。
しかし今はAIがあります。
【共有】webp変換サンプルページ
格安レンタルサーバーであること、そして一括を実現するということで、AIからの返答はPHPの標準画像処理ライブラリでのエンコードを提案してもらいました。
要件は以下です。
- PHPファイル1つアップロードするだけ
- 複数枚を一括変換できる
- 細かい設定不要(品質スライダーだけ)

画像をwebpに変換(サンプルページ)
https://astrowave.jp/amnesia_record/webp_conv.php
※変換は画像のみです。
※Squooshみたいに比べる機能はありません。
※png画像がpng-8は変換不可です。png-24にしてどうそ。
品質: 80%で試してみると、十分な画質&軽量数値だなと感じると思います。
※保存するような機能もありませんのでリロードすると全部消えます。
Google謹製のlibwebpオープンソースを使うのは、あなたには無理ですね。
どうしても最高画質で細かい調整がしたいときは、おとなしくSquooshを利用しましょう。
10枚でも20枚でも数秒で変換完了です。
こんな人にオススメ
- ブログ記事用の画像(複数枚)
- 商品画像の一括変換
- 日常的な画像処理
- サクッと変換したい
webp_conv.php
<?php
/**
* WebP画像変換ツール(シンプル版)
* Node.js不要・PHP単体で動作
*/
// POSTリクエスト(画像変換API)
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
header('Content-Type: application/json');
try {
// 画像ファイルのチェック
if (!isset($_FILES['image'])) {
throw new Exception('画像ファイルがアップロードされていません。');
}
$file = $_FILES['image'];
// エラーチェック
if ($file['error'] !== UPLOAD_ERR_OK) {
throw new Exception('ファイルのアップロードに失敗しました。');
}
// ファイルサイズチェック(最大10MB)
$maxSize = 10 * 1024 * 1024;
if ($file['size'] > $maxSize) {
throw new Exception('ファイルサイズが大きすぎます(最大10MB)。');
}
// MIMEタイプチェック
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
finfo_close($finfo);
if (!in_array($mimeType, $allowedTypes)) {
throw new Exception('対応していない画像形式です(JPG、PNG、GIFのみ)。');
}
// 品質の取得
$quality = isset($_POST['quality']) ? intval($_POST['quality']) : 80;
$quality = max(1, min(100, $quality));
// 画像の読み込み
$image = null;
switch ($mimeType) {
case 'image/jpeg':
$image = imagecreatefromjpeg($file['tmp_name']);
break;
case 'image/png':
$image = imagecreatefrompng($file['tmp_name']);
// PNG-8(256色以下)のチェック
$colorTotal = imagecolorstotal($image);
if ($colorTotal > 0 && $colorTotal <= 256) {
imagedestroy($image);
throw new Exception('PNG-8形式は対応していません。PNG-24/32形式で保存し直してください。');
}
imagealphablending($image, true);
imagesavealpha($image, true);
break;
case 'image/gif':
$image = imagecreatefromgif($file['tmp_name']);
imagealphablending($image, true);
imagesavealpha($image, true);
break;
}
if (!$image) {
throw new Exception('画像の読み込みに失敗しました。');
}
// WebP変換が利用可能かチェック
if (!function_exists('imagewebp')) {
imagedestroy($image);
throw new Exception('サーバーがWebP変換に対応していません。');
}
// WebP形式で出力
ob_start();
imagewebp($image, null, $quality);
$webpData = ob_get_clean();
imagedestroy($image);
// Base64エンコードして返す
$base64 = base64_encode($webpData);
$originalSize = $file['size'];
$newSize = strlen($webpData);
echo json_encode([
'success' => true,
'filename' => pathinfo($file['name'], PATHINFO_FILENAME) . '.webp',
'data' => $base64,
'originalSize' => $originalSize,
'newSize' => $newSize,
'reduction' => round(($originalSize - $newSize) / $originalSize * 100, 1)
]);
} catch (Exception $e) {
http_response_code(400);
echo json_encode([
'success' => false,
'error' => $e->getMessage()
]);
}
exit;
}
// GETリクエスト(HTMLページ表示)
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-P74VLK9');</script>
<!-- End Google Tag Manager -->
<!-- Google tag (gtag.js) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-L2YK2LMJ0Y"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-L2YK2LMJ0Y');
//gtag('config', 'UA-30305859-1');
</script>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="author" content="宇宙デザイン・近未来デザイン作成のアストロウェーブ" />
<meta name="keywords" content="宇宙,デザイン,近未来,アストロウェーブ,web,ホームページ,空想" />
<meta name="description" content="宇宙生活を楽しくデザインしたいサイト「アストロウェーブ」" />
<link rel="icon" href="https://neo.astrowave.jp/wp-content/uploads/2022/04/cropped-siteicon.png" sizes="32x32" />
<link rel="icon" href="https://neo.astrowave.jp/wp-content/uploads/2022/04/cropped-siteicon.png" sizes="192x192" />
<link rel="apple-touch-icon" href="https://neo.astrowave.jp/wp-content/uploads/2022/04/cropped-siteicon.png" />
<!-- OG -->
<meta property="og:type" content="blog">
<meta property="og:title" content="宇宙生活を楽しくデザインしたいサイト「アストロウェーブ」">
<meta property="og:url" content="<?php echo (empty($_SERVER["HTTPS"]) ? "http://" : "https://") . $_SERVER["HTTP_HOST"] . $_SERVER["REQUEST_URI"]; ?>">
<meta property="og:description" content="宇宙生活を楽しくデザインしたいサイト「アストロウェーブ」">
<meta property="og:image" content="https://neo.astrowave.jp/wp-content/uploads/2023/04/tsuzuri_main.jpg">
<meta property="og:site_name" content="アストロウェーブ">
<meta name="twitter:card" content="summary">
<meta name="twitter:site" content="@Saigamo_a">
<title>WebP画像変換ツール</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #4f46e5;
--primary-hover: #4338ca;
--success: #10b981;
--error: #ef4444;
--bg: #f9fafb;
--card: #ffffff;
--text: #111827;
--text-light: #6b7280;
--border: #e5e7eb;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, var(--primary) 0%, #7c3aed 100%);
color: white;
text-align: center;
padding: 3rem 2rem;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.header h1 {
font-size: 2.5rem;
margin-bottom: 0.5rem;
}
.subtitle {
font-size: 1.1rem;
opacity: 0.9;
}
.subtitle a {
color: white;
text-decoration: underline;
font-size: 18px;
transition: opacity 0.3s ease;
}
.subtitle a:hover {
text-decoration: none;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
}
.upload-area {
background: var(--card);
border: 3px dashed var(--border);
border-radius: 1rem;
padding: 4rem 2rem;
text-align: center;
transition: all 0.3s ease;
cursor: pointer;
margin-bottom: 2rem;
}
.upload-area:hover {
border-color: var(--primary);
background: #fafbff;
}
.upload-area.dragover {
border-color: var(--primary);
background: #f0f4ff;
transform: scale(1.02);
}
.upload-icon {
width: 80px;
height: 80px;
margin: 0 auto 1rem;
color: var(--primary);
}
.btn-primary {
background: var(--primary);
color: white;
border: none;
padding: 0.75rem 2rem;
border-radius: 0.5rem;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
margin: 1rem 0;
}
.btn-primary:hover {
background: var(--primary-hover);
transform: translateY(-2px);
}
.settings-card {
background: var(--card);
padding: 2rem;
border-radius: 1rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-bottom: 2rem;
display: none;
}
.settings-card.active {
display: block;
}
.setting-item {
margin-bottom: 1.5rem;
}
.setting-item label {
display: block;
font-weight: 600;
margin-bottom: 0.5rem;
}
input[type="range"] {
width: 100%;
height: 8px;
border-radius: 5px;
background: var(--border);
outline: none;
-webkit-appearance: none;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
width: 24px;
height: 24px;
border-radius: 50%;
background: var(--primary);
cursor: pointer;
}
.btn-convert {
background: var(--success);
color: white;
border: none;
padding: 1rem 3rem;
border-radius: 0.5rem;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
width: 100%;
margin-top: 1.5rem;
transition: all 0.2s ease;
}
.btn-convert:hover:not(:disabled) {
background: #059669;
transform: translateY(-2px);
}
.btn-convert:disabled {
background: var(--border);
cursor: not-allowed;
}
.preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 1.5rem;
margin-top: 1.5rem;
}
.preview-card {
background: var(--card);
border-radius: 0.75rem;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
position: relative;
}
.preview-card:hover {
transform: translateY(-4px);
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
}
.preview-image {
width: 100%;
height: 200px;
object-fit: cover;
background: #f3f4f6;
}
.preview-info {
padding: 1rem;
}
.preview-filename {
font-weight: 600;
margin-bottom: 0.25rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.preview-size {
font-size: 0.9rem;
color: var(--text-light);
}
.status-badge {
display: inline-block;
padding: 0.25rem 0.75rem;
border-radius: 9999px;
font-size: 0.85rem;
font-weight: 600;
margin-top: 0.5rem;
}
.status-pending {
background: #e5e7eb;
color: #6b7280;
}
.status-converting {
background: #dbeafe;
color: #1e40af;
}
.status-success {
background: #d1fae5;
color: #065f46;
}
.status-error {
background: #fee2e2;
color: #991b1b;
}
.btn-remove {
position: absolute;
top: 0.5rem;
right: 0.5rem;
background: rgba(239, 68, 68, 0.9);
color: white;
border: none;
width: 28px;
height: 28px;
border-radius: 50%;
cursor: pointer;
font-size: 1rem;
opacity: 0;
transition: all 0.2s ease;
}
.preview-card:hover .btn-remove {
opacity: 1;
}
.btn-remove:hover {
background: #dc2626;
transform: scale(1.1);
}
.btn-download {
background: var(--primary);
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
font-size: 0.9rem;
cursor: pointer;
margin-top: 0.5rem;
width: 100%;
}
.btn-download:hover {
background: var(--primary-hover);
}
.download-all-section {
background: var(--card);
padding: 1.5rem;
border-radius: 1rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin-top: 2rem;
text-align: center;
display: none;
}
.download-all-section.active {
display: block;
}
.btn-download-all {
background: linear-gradient(135deg, var(--success) 0%, #059669 100%);
color: white;
border: none;
padding: 1rem 3rem;
border-radius: 0.5rem;
font-size: 1.1rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s ease;
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.3);
}
.btn-download-all:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(16, 185, 129, 0.4);
}
.download-all-section p {
color: var(--text-light);
margin-bottom: 1rem;
}
.progress-section {
background: var(--card);
padding: 2rem;
border-radius: 1rem;
text-align: center;
margin-bottom: 2rem;
display: none;
}
.progress-section.active {
display: block;
}
.progress-bar {
width: 100%;
height: 24px;
background: var(--border);
border-radius: 12px;
overflow: hidden;
margin: 1rem 0;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), #7c3aed);
transition: width 0.3s ease;
border-radius: 12px;
}
.hint {
color: var(--text-light);
font-size: 0.9rem;
}
@media (max-width: 768px) {
.header h1 {
font-size: 1.8rem;
}
.container {
padding: 1rem;
}
.preview-grid {
grid-template-columns: 1fr;
}
}
/* フッター(通常の高さ、スタイル) */
#footer {
width: 100%;
background: linear-gradient(45deg, #1abc9c, #16a085);
color: white;
padding: 100px 0 50px;
text-align: center;
}
.footer-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 20px;
}
.footer-title {
font-size: 2.5rem;
font-weight: bold;
margin-bottom: 2rem;
}
.footer-subtitle {
font-size: 1.2rem;
opacity: 0.8;
margin-bottom: 3rem;
}
.footer-subtitle a {
color: white;
text-decoration: underline;
font-size: 18px;
transition: opacity 0.3s ease;
}
.footer-subtitle a:hover {
text-decoration: none;
}
.footer-links {
border-top: 1px solid rgba(255, 255, 255, 0.2);
padding-top: 50px;
margin-top: 50px;
}
.footer-links ul {
list-style: none;
display: flex;
justify-content: center;
gap: 30px;
flex-wrap: wrap;
margin-bottom: 30px;
}
.footer-links a {
color: white;
text-decoration: none;
font-size: 14px;
opacity: 0.8;
transition: opacity 0.3s ease;
}
.footer-links a:hover {
opacity: 1;
}
.footer-copyright {
font-size: 12px;
opacity: 0.6;
margin-top: 20px;
}
.footer-title {
font-size: 1.8rem;
}
.footer-subtitle {
font-size: 1rem;
}
</style>
</head>
<body>
<header class="header">
<h1>WebP画像変換ツール</h1>
<p class="subtitle">-<a href="https://neo.astrowave.jp/blog/22698/" target="_blank">使い方はこちら</a>-</p>
</header>
<main class="container">
<!-- アップロードエリア -->
<div class="upload-area" id="uploadArea">
<input type="file" id="fileInput" multiple accept="image/jpeg,image/png,image/gif" hidden>
<svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
<h2>画像をドラッグ&ドロップ</h2>
<p>または</p>
<button class="btn-primary" id="selectFilesBtn">ファイルを選択</button>
<p class="hint">対応形式: JPG, PNG, GIF(複数選択可)</p>
</div>
<!-- 設定エリア -->
<div class="settings-card" id="settingsCard">
<h3>⚙️ 変換設定</h3>
<div class="setting-item">
<label>品質: <span id="qualityValue">80</span>%</label>
<input type="range" id="qualitySlider" min="1" max="100" value="80">
<p class="hint">低い値 = ファイルサイズ小 / 高い値 = 画質良</p>
</div>
<button class="btn-convert" id="convertBtn">🔄 すべて変換</button>
</div>
<!-- 進捗表示 -->
<div class="progress-section" id="progressSection">
<h3>変換中...</h3>
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
<p id="progressText">0 / 0 完了</p>
</div>
<!-- プレビューエリア -->
<div id="previewSection">
<h3 style="display: none;" id="previewTitle">📋 画像一覧</h3>
<div class="preview-grid" id="previewGrid"></div>
</div>
<!-- まとめてダウンロード -->
<div class="download-all-section" id="downloadAllSection">
<h3>✅ 変換完了!</h3>
<p><span id="completedCount">0</span>個の画像をWebPに変換しました</p>
<button class="btn-download-all" id="downloadAllBtn">📦 すべてまとめてダウンロード</button>
</div>
</main>
<!-- フッター -->
<div id="footer">
<div class="footer-content">
<h2 class="footer-title">WebP画像変換ツール</h2>
<p class="footer-subtitle"><a href="https://neo.astrowave.jp/blog/22698/" target="_blank">使い方</a></p>
<div class="footer-links">
<ul>
<li><a href="https://astrowave.jp/amnesia_record/">健忘録</a></li>
<li><a href="https://astrowave.jp">アストロウェーブ</a></li>
<li><a href="https://neo.astrowave.jp/privacy-policy/">PRIVACY POLICY</a></li>
<li><a href="https://neo.astrowave.jp/saigamo-profile/">ABOUT</a></li>
<li><a href="https://neo.astrowave.jp/otoiawase/">お問い合わせ</a></li>
</ul>
<div class="footer-copyright">
© <script type="text/javascript">var date = new Date();var year = date.getFullYear();document.write(year);</script> astrowave.
</div>
</div>
</div>
</div>
<script>
class WebPConverter {
constructor() {
this.files = [];
this.quality = 80;
this.results = [];
this.initElements();
this.attachEvents();
}
initElements() {
this.uploadArea = document.getElementById('uploadArea');
this.fileInput = document.getElementById('fileInput');
this.selectFilesBtn = document.getElementById('selectFilesBtn');
this.qualitySlider = document.getElementById('qualitySlider');
this.qualityValue = document.getElementById('qualityValue');
this.convertBtn = document.getElementById('convertBtn');
this.settingsCard = document.getElementById('settingsCard');
this.previewSection = document.getElementById('previewSection');
this.previewTitle = document.getElementById('previewTitle');
this.previewGrid = document.getElementById('previewGrid');
this.progressSection = document.getElementById('progressSection');
this.progressFill = document.getElementById('progressFill');
this.progressText = document.getElementById('progressText');
this.downloadAllSection = document.getElementById('downloadAllSection');
this.downloadAllBtn = document.getElementById('downloadAllBtn');
this.completedCount = document.getElementById('completedCount');
}
attachEvents() {
this.selectFilesBtn.addEventListener('click', () => this.fileInput.click());
this.fileInput.addEventListener('change', (e) => this.handleFiles(Array.from(e.target.files)));
this.uploadArea.addEventListener('dragover', (e) => {
e.preventDefault();
this.uploadArea.classList.add('dragover');
});
this.uploadArea.addEventListener('dragleave', () => {
this.uploadArea.classList.remove('dragover');
});
this.uploadArea.addEventListener('drop', (e) => {
e.preventDefault();
this.uploadArea.classList.remove('dragover');
const files = Array.from(e.dataTransfer.files).filter(f => f.type.startsWith('image/'));
this.handleFiles(files);
});
this.qualitySlider.addEventListener('input', (e) => {
this.quality = parseInt(e.target.value);
this.qualityValue.textContent = this.quality;
});
this.convertBtn.addEventListener('click', () => this.convertAll());
this.downloadAllBtn.addEventListener('click', () => this.downloadAll());
}
handleFiles(files) {
if (files.length === 0) return;
this.files = files;
this.results = [];
this.displayPreviews();
this.settingsCard.classList.add('active');
this.previewTitle.style.display = 'block';
this.downloadAllSection.classList.remove('active');
}
displayPreviews() {
this.previewGrid.innerHTML = '';
this.files.forEach((file, index) => {
const reader = new FileReader();
reader.onload = (e) => {
const card = document.createElement('div');
card.className = 'preview-card';
card.innerHTML = `
<img src="${e.target.result}" alt="${file.name}" class="preview-image">
<div class="preview-info">
<div class="preview-filename">${file.name}</div>
<div class="preview-size">${this.formatSize(file.size)}</div>
<div id="status-${index}">
<span class="status-badge status-pending">待機中</span>
</div>
</div>
<button class="btn-remove" onclick="converter.removeFile(${index})">✕</button>
`;
this.previewGrid.appendChild(card);
};
reader.readAsDataURL(file);
});
}
removeFile(index) {
this.files = Array.from(this.files).filter((_, i) => i !== index);
if (this.files.length === 0) {
this.settingsCard.classList.remove('active');
this.previewTitle.style.display = 'none';
}
this.displayPreviews();
}
async convertAll() {
this.convertBtn.disabled = true;
this.progressSection.classList.add('active');
this.downloadAllSection.classList.remove('active');
let completed = 0;
const total = this.files.length;
for (let i = 0; i < this.files.length; i++) {
await this.convertImage(this.files[i], i);
completed++;
this.updateProgress(completed, total);
}
this.convertBtn.disabled = false;
setTimeout(() => {
this.progressSection.classList.remove('active');
// 成功した変換があれば、まとめてダウンロードボタンを表示
if (this.results.length > 0) {
this.completedCount.textContent = this.results.length;
this.downloadAllSection.classList.add('active');
// スクロールしてボタンを表示
this.downloadAllSection.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
}, 1000);
}
async convertImage(file, index) {
const statusEl = document.getElementById(`status-${index}`);
try {
statusEl.innerHTML = '<span class="status-badge status-converting">変換中...</span>';
const formData = new FormData();
formData.append('image', file);
formData.append('quality', this.quality);
const response = await fetch('webp_conv.php', {
method: 'POST',
body: formData
});
const result = await response.json();
if (!result.success) {
throw new Error(result.error);
}
this.results.push({
filename: result.filename,
data: result.data
});
statusEl.innerHTML = `
<span class="status-badge status-success">✓ 完了</span>
<div class="preview-size">${this.formatSize(result.originalSize)} → ${this.formatSize(result.newSize)}</div>
<div class="preview-size" style="color: var(--success); font-weight: 600;">${result.reduction}% 削減</div>
<button class="btn-download" onclick="converter.download(${index})">💾 ダウンロード</button>
`;
} catch (error) {
console.error('変換エラー:', error);
statusEl.innerHTML = `<span class="status-badge status-error">✕ ${error.message}</span>`;
}
}
download(index) {
const result = this.results[index];
if (!result) return;
const blob = this.base64ToBlob(result.data, 'image/webp');
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = result.filename;
a.click();
URL.revokeObjectURL(url);
}
downloadAll() {
if (this.results.length === 0) return;
// 複数ファイルを順次ダウンロード
this.results.forEach((result, index) => {
setTimeout(() => {
const blob = this.base64ToBlob(result.data, 'image/webp');
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = result.filename;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
// メモリ解放
setTimeout(() => URL.revokeObjectURL(url), 100);
}, index * 300); // 300msごとにダウンロード
});
// フィードバック表示
const originalText = this.downloadAllBtn.textContent;
this.downloadAllBtn.textContent = '📥 ダウンロード中...';
this.downloadAllBtn.disabled = true;
setTimeout(() => {
this.downloadAllBtn.textContent = '✅ ダウンロード完了!';
setTimeout(() => {
this.downloadAllBtn.textContent = originalText;
this.downloadAllBtn.disabled = false;
}, 2000);
}, this.results.length * 300 + 500);
}
base64ToBlob(base64, mimeType) {
const byteCharacters = atob(base64);
const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += 512) {
const slice = byteCharacters.slice(offset, offset + 512);
const byteNumbers = new Array(slice.length);
for (let i = 0; i < slice.length; i++) {
byteNumbers[i] = slice.charCodeAt(i);
}
byteArrays.push(new Uint8Array(byteNumbers));
}
return new Blob(byteArrays, { type: mimeType });
}
updateProgress(completed, total) {
const percent = (completed / total) * 100;
this.progressFill.style.width = percent + '%';
this.progressText.textContent = `${completed} / ${total} 完了`;
}
formatSize(bytes) {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
}
const converter = new WebPConverter();
</script>
</body>
</html>
・画質のデフォルトは80%にしています。
・一括ダウンロードはZIPせずに、複数ファイルを順次ダウンロードする方式を採用しています。
必要な環境
PHP 7.4以上
GD拡張機能(WebP対応)
パフォーマンスとセキュリティ
ファイルタイプの検証: MIMEタイプをサーバー側で厳密にチェック
エラーハンドリング: 不正なファイルを適切に拒否
--------php--------
// MIMEタイプチェック
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif'];
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mimeType = finfo_file($finfo, $file['tmp_name']);
if (!in_array($mimeType, $allowedTypes)) {
throw new Exception('対応していない画像形式です');
}
--------------------
カスタマイズ方法
ファイルサイズ制限: デフォルトで10MBまで(変更可能)
最大ファイルサイズの変更
--------php--------
// デフォルト: 10MB
$maxSize = 10 * 1024 * 1024;
// 20MBに変更する場合
$maxSize = 20 * 1024 * 1024;
--------------------
デフォルト品質の変更
--------javascript--------
// デフォルト: 80%
this.quality = 80;
// 90%に変更する場合
this.quality = 90;
--------------------
対応形式の追加
WebP形式の画像を入力として受け付ける場合:
--------php--------
$allowedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'];
詳しくはAIに放り込んでください。
まさに自分で使うため。
ブログのサイドバーにリンクを付けようと思います。
【AI】イラストを描いてもらった
今回の記事のキャッチ画像で使わせてもらいます「Google ImageFX」で作成した画像です。誰でもgoogleアカウントでログインして使えます。
この記事にピッタリなイラストのための考えたリクエストは、「サイバー空間で膨大なデータを懸命に圧縮するメイド型ロボット。メイドは女性で、日本のアニメ風。髪の毛はピンク。」です。


すごい高機能なメイドさんの雰囲気がしますよ。
こういうメイドさんが好きなんですか。
星間旅路のメロディ
「宇宙の静けさに包まれながら、漂流する過去の音楽を捜し求め、銀河の奥底でその旋律に耳を傾ける。」
「この電波はどこの星からきたのだろうか。」
どこかで聞いたことがあるような。



