Skip to content

Commit c09f49b

Browse files
committed
feat: 添加图片与Base64双向转换功能
- 在图片转Base64工具中添加Base64转图片功能 - 支持Data URI和纯Base64字符串两种输入格式 - 添加模式切换按钮,可以在两种模式之间切换 - 支持预览和下载转换后的图片 - 修复本地启动时的链接路径问题 - 更新工具标题和描述为'图片 Base64 互转'
1 parent 202c106 commit c09f49b

2 files changed

Lines changed: 314 additions & 16 deletions

File tree

tools/image-to-base64/app.html

Lines changed: 304 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<link rel="icon" href="../../favicon.svg">
55
<meta charset="UTF-8">
66
<meta name="viewport" content="width=device-width, initial-scale=1.0">
7-
<title>图片转 Base64</title>
7+
<title>图片 Base64 互转</title>
88
<link rel="canonical" href="https://www.htmls.dev/tools/image-to-base64/app.html">
99
<style>
1010
* {
@@ -36,6 +36,37 @@
3636
margin-bottom: 10px;
3737
}
3838

39+
.mode-toggle {
40+
display: flex;
41+
justify-content: center;
42+
gap: 10px;
43+
margin-bottom: 30px;
44+
}
45+
46+
.mode-btn {
47+
padding: 12px 30px;
48+
background: rgba(255, 255, 255, 0.2);
49+
border: 2px solid rgba(255, 255, 255, 0.3);
50+
color: white;
51+
border-radius: 10px;
52+
cursor: pointer;
53+
font-size: 1rem;
54+
font-weight: 600;
55+
transition: all 0.3s;
56+
}
57+
58+
.mode-btn:hover {
59+
background: rgba(255, 255, 255, 0.3);
60+
transform: translateY(-2px);
61+
}
62+
63+
.mode-btn.active {
64+
background: rgba(255, 255, 255, 0.95);
65+
color: #ff9a9e;
66+
border-color: rgba(255, 255, 255, 0.95);
67+
box-shadow: 0 5px 15px rgba(255, 154, 158, 0.4);
68+
}
69+
3970
.card {
4071
background: rgba(255, 255, 255, 0.95);
4172
border-radius: 20px;
@@ -195,6 +226,64 @@
195226
border-color: #ff9a9e;
196227
}
197228

229+
.input-section {
230+
display: none;
231+
}
232+
233+
.input-section.active {
234+
display: block;
235+
}
236+
237+
.base64-input-section {
238+
margin-top: 30px;
239+
}
240+
241+
.base64-input-header {
242+
display: flex;
243+
justify-content: space-between;
244+
align-items: center;
245+
margin-bottom: 15px;
246+
}
247+
248+
.base64-input-header h3 {
249+
color: #333;
250+
font-size: 1.2rem;
251+
}
252+
253+
.base64-textarea {
254+
width: 100%;
255+
min-height: 200px;
256+
padding: 15px;
257+
border: 2px solid #e0e0e0;
258+
border-radius: 12px;
259+
font-family: 'Courier New', monospace;
260+
font-size: 12px;
261+
resize: vertical;
262+
}
263+
264+
.base64-textarea:focus {
265+
outline: none;
266+
border-color: #ff9a9e;
267+
}
268+
269+
.base64-preview-section {
270+
margin-top: 20px;
271+
display: none;
272+
}
273+
274+
.base64-preview-section.active {
275+
display: block;
276+
}
277+
278+
.error-message {
279+
background: #fee;
280+
color: #c33;
281+
padding: 15px;
282+
border-radius: 8px;
283+
margin-top: 15px;
284+
border-left: 4px solid #c33;
285+
}
286+
198287
.format-options {
199288
display: flex;
200289
gap: 15px;
@@ -232,19 +321,26 @@
232321
<body>
233322
<div class="container">
234323
<div class="header">
235-
<h1>图片转 Base64</h1>
236-
<p>将图片转换为 Base64 编码字符串</p>
324+
<h1>图片 Base64 互转</h1>
325+
<p>图片与 Base64 编码字符串之间的双向转换</p>
326+
</div>
327+
328+
<div class="mode-toggle">
329+
<button class="mode-btn active" data-mode="image-to-base64" onclick="setMode('image-to-base64')">图片转 Base64</button>
330+
<button class="mode-btn" data-mode="base64-to-image" onclick="setMode('base64-to-image')">Base64 转图片</button>
237331
</div>
238332

239333
<div class="card">
240-
<div class="upload-area" id="upload-area" onclick="document.getElementById('file-input').click()">
241-
<div class="upload-icon">📷</div>
242-
<div class="upload-text">点击上传或拖拽图片到此处</div>
243-
<div class="upload-hint">支持 JPG、PNG、GIF、WebP 等格式</div>
244-
</div>
245-
<input type="file" id="file-input" class="file-input" accept="image/*" onchange="handleFile(this.files[0])">
334+
<!-- 图片转Base64模式 -->
335+
<div class="input-section active" id="image-to-base64-section">
336+
<div class="upload-area" id="upload-area" onclick="document.getElementById('file-input').click()">
337+
<div class="upload-icon">📷</div>
338+
<div class="upload-text">点击上传或拖拽图片到此处</div>
339+
<div class="upload-hint">支持 JPG、PNG、GIF、WebP 等格式</div>
340+
</div>
341+
<input type="file" id="file-input" class="file-input" accept="image/*" onchange="handleFile(this.files[0])">
246342

247-
<div class="preview-section" id="preview-section">
343+
<div class="preview-section" id="preview-section">
248344
<div class="image-preview">
249345
<img id="preview-img" class="preview-img" alt="预览图片">
250346
<div class="image-info">
@@ -291,16 +387,82 @@ <h3>Base64 输出</h3>
291387
<textarea class="output-textarea" id="output-text" readonly></textarea>
292388
</div>
293389
</div>
390+
</div>
391+
392+
<!-- Base64转图片模式 -->
393+
<div class="input-section" id="base64-to-image-section">
394+
<div class="base64-input-section">
395+
<div class="base64-input-header">
396+
<h3>Base64 输入</h3>
397+
<button class="btn btn-secondary" onclick="clearBase64Input()">清空</button>
398+
</div>
399+
<textarea class="base64-textarea" id="base64-input" placeholder="粘贴 Base64 字符串或 Data URI(如:data:image/png;base64,iVBORw0KG...)"></textarea>
400+
<button class="btn btn-primary" style="width: 100%; margin-top: 15px;" onclick="convertBase64ToImage()">转换为图片</button>
401+
<div id="base64-error" class="error-message" style="display: none;"></div>
402+
</div>
403+
404+
<div class="base64-preview-section" id="base64-preview-section">
405+
<div class="image-preview">
406+
<img id="base64-preview-img" class="preview-img" alt="预览图片">
407+
<div class="image-info">
408+
<div class="info-item">
409+
<div class="info-label">图片格式</div>
410+
<div class="info-value" id="base64-image-format">-</div>
411+
</div>
412+
<div class="info-item">
413+
<div class="info-label">尺寸</div>
414+
<div class="info-value" id="base64-image-dimensions">-</div>
415+
</div>
416+
<div class="info-item">
417+
<div class="info-label">Base64 大小</div>
418+
<div class="info-value" id="base64-input-size">-</div>
419+
</div>
420+
<div class="info-item">
421+
<div class="info-label">图片大小</div>
422+
<div class="info-value" id="base64-image-size">-</div>
423+
</div>
424+
</div>
425+
</div>
426+
427+
<div class="output-section">
428+
<div class="output-header">
429+
<h3>操作</h3>
430+
<div class="output-actions">
431+
<button class="btn btn-primary" onclick="downloadBase64Image()">下载图片</button>
432+
<button class="btn btn-secondary" onclick="resetBase64Preview()">重新转换</button>
433+
</div>
434+
</div>
435+
</div>
436+
</div>
437+
</div>
294438
</div>
295439
</div>
296440

297441
<script>
298442
let currentBase64 = '';
299443
let currentMimeType = '';
300444
let currentFileName = '';
445+
let currentMode = 'image-to-base64';
446+
let base64ImageBlob = null;
447+
let base64ImageMimeType = '';
448+
let base64PreviewImageUrl = null;
301449

302450
const uploadArea = document.getElementById('upload-area');
303451

452+
function setMode(mode) {
453+
currentMode = mode;
454+
document.querySelectorAll('.mode-btn').forEach(btn => {
455+
btn.classList.toggle('active', btn.dataset.mode === mode);
456+
});
457+
458+
document.getElementById('image-to-base64-section').classList.toggle('active', mode === 'image-to-base64');
459+
document.getElementById('base64-to-image-section').classList.toggle('active', mode === 'base64-to-image');
460+
461+
if (mode === 'base64-to-image') {
462+
document.getElementById('base64-input').focus();
463+
}
464+
}
465+
304466
// 拖拽事件
305467
uploadArea.addEventListener('dragover', (e) => {
306468
e.preventDefault();
@@ -407,6 +569,129 @@ <h3>Base64 输出</h3>
407569
currentFileName = '';
408570
}
409571

572+
function convertBase64ToImage() {
573+
const base64Input = document.getElementById('base64-input').value.trim();
574+
const errorDiv = document.getElementById('base64-error');
575+
576+
if (!base64Input) {
577+
errorDiv.textContent = '请输入 Base64 字符串';
578+
errorDiv.style.display = 'block';
579+
return;
580+
}
581+
582+
try {
583+
let base64String = base64Input;
584+
585+
// 处理 Data URI 格式(data:image/png;base64,xxxxx)
586+
if (base64Input.startsWith('data:')) {
587+
const commaIndex = base64Input.indexOf(',');
588+
if (commaIndex === -1) {
589+
throw new Error('无效的 Data URI 格式');
590+
}
591+
592+
const header = base64Input.substring(0, commaIndex);
593+
const mimeMatch = header.match(/data:([^;]+)/);
594+
if (mimeMatch) {
595+
base64ImageMimeType = mimeMatch[1];
596+
}
597+
base64String = base64Input.substring(commaIndex + 1);
598+
} else {
599+
// 纯 Base64 字符串,尝试从常见格式推断
600+
base64ImageMimeType = 'image/png'; // 默认 PNG
601+
}
602+
603+
// 清理可能的空白字符
604+
base64String = base64String.replace(/\s/g, '');
605+
606+
// 验证 Base64 格式
607+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(base64String)) {
608+
throw new Error('无效的 Base64 字符串格式');
609+
}
610+
611+
// 转换为 Blob
612+
const byteCharacters = atob(base64String);
613+
const byteNumbers = new Array(byteCharacters.length);
614+
for (let i = 0; i < byteCharacters.length; i++) {
615+
byteNumbers[i] = byteCharacters.charCodeAt(i);
616+
}
617+
const byteArray = new Uint8Array(byteNumbers);
618+
base64ImageBlob = new Blob([byteArray], { type: base64ImageMimeType });
619+
620+
// 释放之前的图片 URL
621+
if (base64PreviewImageUrl) {
622+
URL.revokeObjectURL(base64PreviewImageUrl);
623+
}
624+
625+
// 创建图片 URL 并显示预览
626+
base64PreviewImageUrl = URL.createObjectURL(base64ImageBlob);
627+
const img = new Image();
628+
629+
img.onload = function() {
630+
document.getElementById('base64-preview-img').src = base64PreviewImageUrl;
631+
document.getElementById('base64-preview-section').classList.add('active');
632+
633+
// 显示图片信息
634+
const format = base64ImageMimeType.split('/')[1] || '未知';
635+
document.getElementById('base64-image-format').textContent = format.toUpperCase();
636+
document.getElementById('base64-image-dimensions').textContent = `${img.width} x ${img.height}`;
637+
document.getElementById('base64-input-size').textContent = formatBytes(base64Input.length);
638+
document.getElementById('base64-image-size').textContent = formatBytes(base64ImageBlob.size);
639+
640+
errorDiv.style.display = 'none';
641+
};
642+
643+
img.onerror = function() {
644+
URL.revokeObjectURL(base64PreviewImageUrl);
645+
base64PreviewImageUrl = null;
646+
throw new Error('无法解析为有效图片,请检查 Base64 字符串是否正确');
647+
};
648+
649+
img.src = base64PreviewImageUrl;
650+
651+
} catch (error) {
652+
errorDiv.textContent = '错误:' + error.message;
653+
errorDiv.style.display = 'block';
654+
document.getElementById('base64-preview-section').classList.remove('active');
655+
}
656+
}
657+
658+
function downloadBase64Image() {
659+
if (!base64ImageBlob) {
660+
showToast('没有可下载的图片');
661+
return;
662+
}
663+
664+
const url = URL.createObjectURL(base64ImageBlob);
665+
const a = document.createElement('a');
666+
a.href = url;
667+
668+
// 根据 MIME 类型确定文件扩展名
669+
const extension = base64ImageMimeType.split('/')[1] || 'png';
670+
a.download = `image.${extension}`;
671+
672+
a.click();
673+
URL.revokeObjectURL(url);
674+
showToast('已开始下载');
675+
}
676+
677+
function resetBase64Preview() {
678+
document.getElementById('base64-preview-section').classList.remove('active');
679+
document.getElementById('base64-input').value = '';
680+
document.getElementById('base64-error').style.display = 'none';
681+
if (base64PreviewImageUrl) {
682+
URL.revokeObjectURL(base64PreviewImageUrl);
683+
base64PreviewImageUrl = null;
684+
}
685+
base64ImageBlob = null;
686+
base64ImageMimeType = '';
687+
}
688+
689+
function clearBase64Input() {
690+
document.getElementById('base64-input').value = '';
691+
document.getElementById('base64-error').style.display = 'none';
692+
document.getElementById('base64-input').focus();
693+
}
694+
410695
function formatBytes(bytes) {
411696
if (bytes === 0) return '0 B';
412697
const k = 1024;
@@ -445,6 +730,15 @@ <h3>Base64 输出</h3>
445730
}
446731
`;
447732
document.head.appendChild(style);
733+
734+
// 页面加载时初始化
735+
if (document.getElementById('base64-input')) {
736+
document.getElementById('base64-input').addEventListener('keydown', function(e) {
737+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
738+
convertBase64ToImage();
739+
}
740+
});
741+
}
448742
</script>
449743

450744
<script src="/assets/clicks.js" defer></script>

0 commit comments

Comments
 (0)