1<!DOCTYPE html>
2<class="tag">html lang="zh-CN">
3<class="tag">head>
4<class="tag">meta charset="UTF-class="number">8">
5<class="tag">meta name="viewport" content="width=device-width, initial-scale=class="number">1.0">
6<class="tag">meta property="og:title" content="图片压缩 - η空间">
7<class="tag">meta name="description" content="在线压缩 JPG/PNG/WebP 图片,浏览器本地处理,不上传服务器">
8<class="tag">meta name="keywords" content="图片压缩,在线压缩,图片压缩工具,JPG压缩,PNG压缩">
9<class="tag">meta property="og:description" content="在线压缩 JPG/PNG/WebP 图片,浏览器本地处理">
10<class="tag">meta property="og:type" content="website">
11<class="tag">title>图片压缩 - η空间</class="tag">title>
12<class="tag">link rel="stylesheet" href="/tools/share.css">
13 <class="tag">link rel="stylesheet" href="/assets/pet-cat.css">
14<class="tag">style>
15* { margin: class="number">0; padding: class="number">0; box-sizing: border-box; }
16body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; background: class=class="string">"keyword">class=class="string">"comment">#f0f2f5; color: #class="number">333; line-height: class="number">1.6; }
17class=class="string">"keyword">header {
18 background: linear-gradient(135deg, class=class="string">"keyword">class=class="string">"comment">#667eea class="number">0%, #764ba2 class="number">100%);
19 color: class=class="string">"keyword">class=class="string">"comment">#fff; padding: 30px 20px; text-align: center;
20}
21class=class="string">"keyword">header h1 { font-size: 24px; margin-bottom: 4px; }
22class=class="string">"keyword">header p { opacity: class="number">0.85; font-size: 14px; }
23.container { max-width: 640px; margin: class="number">0 auto; padding: 24px 16px; }
24.back-link { display: inline-block; margin-bottom: 16px; color: class=class="string">"keyword">class=class="string">"comment">#667eea; text-decoration: none; font-size: 14px; }
25.card { background: class=class="string">"keyword">class=class="string">"comment">#fff; border-radius: 12px; padding: 24px; box-shadow: class="number">0 2px 8px rgba(class="number">0,class="number">0,class="number">0,class="number">0.06); margin-bottom: 16px; }
26.upload-zone {
27 border: 2px dashed class=class="string">"keyword">class=class="string">"comment">#ccc;
28 border-radius: 10px;
29 padding: 40px 20px;
30 text-align: center;
31 cursor: pointer;
32 transition: border-color class="number">0.3s, background class="number">0.3s;
33}
34.upload-zone:hover { border-color: class=class="string">"keyword">class=class="string">"comment">#667eea; background: #f8f9ff; }
35.upload-zone .icon { font-size: 48px; margin-bottom: 12px; }
36.upload-zone p { color: class=class="string">"keyword">class=class="string">"comment">#class="number">999; font-size: 14px; }
37.upload-zone input { display: none; }
38.settings { margin-top: 20px; }
39.settings label { display: block; font-size: 14px; font-weight: class="number">600; margin-bottom: 6px; }
40.settings input[type="range"] { width: class="number">100%; margin: 8px class="number">0; }
41.settings .value { font-size: 13px; color: class=class="string">"keyword">class=class="string">"comment">#class="number">666; }
42.preview { display: none; margin-top: 20px; }
43.preview img { max-width: class="number">100%; border-radius: 8px; border: 1px solid class=class="string">"keyword">class=class="string">"comment">#eee; }
44.info { display: flex; gap: 16px; margin-top: 12px; flex-wrap: wrap; }
45.info-item { flex: class="number">1; min-width: 120px; background: class=class="string">"keyword">class=class="string">"comment">#f8f9fa; border-radius: 8px; padding: 12px; text-align: center; }
46.info-item .num { font-size: 18px; font-weight: class="number">700; color: class=class="string">"keyword">class=class="string">"comment">#667eea; }
47.info-item .label { font-size: 12px; color: class=class="string">"keyword">class=class="string">"comment">#class="number">999; margin-top: 2px; }
48.btn {
49 display: inline-block; padding: 10px 24px; border-radius: 8px;
50 border: none; font-size: 15px; cursor: pointer;
51 background: class=class="string">"keyword">class=class="string">"comment">#667eea; color: #fff; transition: opacity class="number">0.2s;
52}
53.btn:hover { opacity: class="number">0.85; }
54.btn:disabled { opacity: class="number">0.4; cursor: not-allowed; }
55.btn-group { margin-top: 16px; display: flex; gap: 10px; flex-wrap: wrap; }
56.format-select { padding: 8px 12px; border: 1px solid class=class="string">"keyword">class=class="string">"comment">#ddd; border-radius: 6px; font-size: 14px; }
57footer { text-align: center; padding: 24px; color: class=class="string">"keyword">class=class="string">"comment">#class="number">999; font-size: 13px; }
58</class="tag">style>
59</class="tag">head>
60<class="tag">body>
61
62<class=class="string">"keyword">header>
63 <class="tag">h1>🖼️ 图片压缩</class="tag">h1>
64 <class="tag">p>浏览器本地处理,图片不会上传到服务器</class="tag">p>
65</class=class="string">"keyword">header>
66
67<class="tag">div class=class="string">"keyword">class="container">
68 <class="tag">a href="index.html" class=class="string">"keyword">class="back-link">← 返回工具列表</class="tag">a> · <class="tag">a href="/download/image-compress.zip" style="color:class=class="string">"keyword">class=class="string">"comment">#667eea;font-size:13px;text-decoration:none;">📦 源码下载</class="tag">a>
69
70 <class="tag">div class=class="string">"keyword">class="card">
71 <class="tag">div class=class="string">"keyword">class="upload-zone" id="uploadZone">
72 <class="tag">div class=class="string">"keyword">class="icon">📁</class="tag">div>
73 <class="tag">p>点击选择图片,或拖拽图片到此处</class="tag">p>
74 <class="tag">input type="file" id="fileInput" accept="image/jpeg,image/png,image/webp">
75 </class="tag">div>
76
77 <class="tag">div class=class="string">"keyword">class="settings">
78 <class="tag">label>压缩质量 <class="tag">span class=class="string">"keyword">class="value" id="qualityLabel">class="number">80%</class="tag">span></class="tag">label>
79 <class="tag">input type="range" id="quality" min="class="number">10" max="class="number">100" value="class="number">80">
80 </class="tag">div>
81
82 <class="tag">div style="margin-top:12px;">
83 <class="tag">label style="font-size:14px;font-weight:class="number">600;">输出格式</class="tag">label>
84 <class="tag">select class=class="string">"keyword">class="format-select" id="formatSelect">
85 <class="tag">option value="jpeg">JPEG</class="tag">option>
86 <class="tag">option value="png">PNG</class="tag">option>
87 <class="tag">option value="webp">WebP</class="tag">option>
88 </class="tag">select>
89 </class="tag">div>
90
91 <class="tag">div class=class="string">"keyword">class="preview" id="preview">
92 <class="tag">img id="previewImg" alt="预览">
93 <class="tag">div class=class="string">"keyword">class="info">
94 <class="tag">div class=class="string">"keyword">class="info-item">
95 <class="tag">div class=class="string">"keyword">class="num" id="origSize">-</class="tag">div>
96 <class="tag">div class=class="string">"keyword">class="label">原始大小</class="tag">div>
97 </class="tag">div>
98 <class="tag">div class=class="string">"keyword">class="info-item">
99 <class="tag">div class=class="string">"keyword">class="num" id="newSize">-</class="tag">div>
100 <class="tag">div class=class="string">"keyword">class="label">压缩后大小</class="tag">div>
101 </class="tag">div>
102 <class="tag">div class=class="string">"keyword">class="info-item">
103 <class="tag">div class=class="string">"keyword">class="num" id="ratio">-</class="tag">div>
104 <class="tag">div class=class="string">"keyword">class="label">压缩比</class="tag">div>
105 </class="tag">div>
106 </class="tag">div>
107 <class="tag">div class=class="string">"keyword">class="btn-group">
108 <class="tag">button class=class="string">"keyword">class="btn" id="downloadBtn" disabled>⬇ 下载压缩图片</class="tag">button>
109 </class="tag">div>
110 </class="tag">div>
111 </class="tag">div>
112</class="tag">div>
113
114<class="tag">footer>
115 <class="tag">p>图片完全在本地浏览器中处理,不会被上传到服务器</class="tag">p>
116</class="tag">footer>
117
118<class="tag">script>
119class=class="string">"keyword">const uploadZone = document.class="func">getElementById(class="string">'uploadZone');
120class=class="string">"keyword">const fileInput = document.class="func">getElementById(class="string">'fileInput');
121class=class="string">"keyword">const preview = document.class="func">getElementById(class="string">'preview');
122class=class="string">"keyword">const previewImg = document.class="func">getElementById(class="string">'previewImg');
123class=class="string">"keyword">const qualitySlider = document.class="func">getElementById(class="string">'quality');
124class=class="string">"keyword">const qualityLabel = document.class="func">getElementById(class="string">'qualityLabel');
125class=class="string">"keyword">const origSize = document.class="func">getElementById(class="string">'origSize');
126class=class="string">"keyword">const newSize = document.class="func">getElementById(class="string">'newSize');
127class=class="string">"keyword">const ratio = document.class="func">getElementById(class="string">'ratio');
128class=class="string">"keyword">const downloadBtn = document.class="func">getElementById(class="string">'downloadBtn');
129class=class="string">"keyword">const formatSelect = document.class="func">getElementById(class="string">'formatSelect');
130
131class=class="string">"keyword">let currentFile = null;
132class=class="string">"keyword">let compressedBlob = null;
133class=class="string">"keyword">let originalFileSize = class="number">0;
134
135uploadZone.class="func">addEventListener(class="string">'click', () => fileInput.click());
136
137uploadZone.class="func">addEventListener(class="string">'dragover', (e) => {
138 e.preventDefault();
139 uploadZone.style.borderColor = class="string">'class="keyword">class="comment">#667eea';
140 uploadZone.style.background = class="string">'class="keyword">class="comment">#f8f9ff';
141});
142uploadZone.class="func">addEventListener(class="string">'dragleave', () => {
143 uploadZone.style.borderColor = class="string">'class="keyword">class="comment">#ccc';
144 uploadZone.style.background = class="string">'';
145});
146uploadZone.class="func">addEventListener(class="string">'drop', (e) => {
147 e.preventDefault();
148 uploadZone.style.borderColor = class="string">'class="keyword">class="comment">#ccc';
149 uploadZone.style.background = class="string">'';
150 class=class="string">"keyword">if (e.dataTransfer.files.length > class="number">0) {
151 handleFile(e.dataTransfer.files[class="number">0]);
152 }
153});
154
155fileInput.class="func">addEventListener(class="string">'change', () => {
156 class=class="string">"keyword">if (fileInput.files.length > class="number">0) handleFile(fileInput.files[class="number">0]);
157});
158
159qualitySlider.class="func">addEventListener(class="string">'input', () => {
160 qualityLabel.textContent = qualitySlider.value + class="string">'%';
161 class=class="string">"keyword">if (currentFile) compressImage();
162});
163
164formatSelect.class="func">addEventListener(class="string">'change', () => {
165 class=class="string">"keyword">if (currentFile) compressImage();
166});
167
168class=class="string">"keyword">function formatSize(bytes) {
169 class=class="string">"keyword">if (bytes < class="number">1024) class=class="string">"keyword">return bytes + class="string">'B';
170 class=class="string">"keyword">if (bytes < class="number">1048576) class=class="string">"keyword">return (bytes / class="number">1024).toFixed(class="number">1) + class="string">'KB';
171 class=class="string">"keyword">return (bytes / class="number">1048576).toFixed(class="number">1) + class="string">'MB';
172}
173
174class=class="string">"keyword">function handleFile(file) {
175 class=class="string">"keyword">if (!file.type.match(/image\/(jpeg|png|webp)/)) {
176 class="func">alert(class="string">'请选择 JPG、PNG 或 WebP 格式的图片');
177 class=class="string">"keyword">return;
178 }
179 currentFile = file;
180 originalFileSize = file.size;
181 origSize.textContent = formatSize(file.size);
182
183 class=class="string">"keyword">const reader = class=class="string">"keyword">new FileReader();
184 reader.onload = (e) => {
185 previewImg.src = e.target.result;
186 preview.style.display = class="string">'block';
187 compressImage();
188 };
189 reader.readAsDataURL(file);
190}
191
192class=class="string">"keyword">function compressImage() {
193 class=class="string">"keyword">if (!currentFile) class=class="string">"keyword">return;
194
195 class=class="string">"keyword">const img = class=class="string">"keyword">new Image();
196 img.onload = () => {
197 class=class="string">"keyword">const canvas = document.createElement(class="string">'canvas');
198 canvas.width = img.width;
199 canvas.height = img.height;
200 class=class="string">"keyword">const ctx = canvas.getContext(class="string">'2d');
201 ctx.drawImage(img, class="number">0, class="number">0);
202
203 class=class="string">"keyword">const quality = qualitySlider.value / class="number">100;
204 class=class="string">"keyword">const format = formatSelect.value;
205 class=class="string">"keyword">let mimeType = class="string">'image/jpeg';
206 class=class="string">"keyword">let ext = class="string">'jpg';
207 class=class="string">"keyword">if (format === class="string">'png') { mimeType = class="string">'image/png'; ext = class="string">'png'; }
208 class=class="string">"keyword">else class=class="string">"keyword">if (format === class="string">'webp') { mimeType = class="string">'image/webp'; ext = class="string">'webp'; }
209
210 canvas.toBlob((blob) => {
211 class=class="string">"keyword">if (!blob) class=class="string">"keyword">return;
212 compressedBlob = blob;
213 newSize.textContent = formatSize(blob.size);
214 class=class="string">"keyword">const pct = ((class="number">1 - blob.size / originalFileSize) * class="number">100).toFixed(class="number">1);
215 ratio.textContent = pct > class="number">0 ? class="string">'-' + pct + class="string">'%' : class="string">'+' + Math.abs(pct) + class="string">'%';
216 ratio.style.color = pct > class="number">0 ? class="string">'class="keyword">class="comment">#22c55e' : class="string">'#ef4444';
217
218 downloadBtn.disabled = false;
219 downloadBtn.onclick = () => {
220 class=class="string">"keyword">const url = URL.createObjectURL(blob);
221 class=class="string">"keyword">const a = document.createElement(class="string">'a');
222 class=class="string">"keyword">const name = currentFile.name.replace(/\.[^.]+$/, class="string">'');
223 a.href = url;
224 a.download = name + class="string">'_compressed.' + ext;
225 document.body.appendChild(a);
226 a.click();
227 document.body.removeChild(a);
228 URL.revokeObjectURL(url);
229 };
230 }, mimeType, quality);
231 };
232 img.src = previewImg.src;
233}
234</class="tag">script>
235<class="tag">script src="/tools/share.js"></class="tag">script>
236<class="tag">script src="/assets/pet-cat.js"></class="tag">script>
237</class="tag">body>
238</class="tag">html>