|
|
|
|
|
<!DOCTYPE html>
|
|
|
|
|
|
<html lang="zh-CN">
|
|
|
|
|
|
<head>
|
|
|
|
|
|
<meta charset="UTF-8">
|
|
|
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
|
|
|
<title>图片裁剪工具</title>
|
|
|
|
|
|
<style>
|
|
|
|
|
|
body {
|
|
|
|
|
|
font-family: Arial, sans-serif;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
background-color: #f5f5f5;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.container {
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
margin: 0;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.header {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.images-grid {
|
|
|
|
|
|
display: grid;
|
|
|
|
|
|
grid-template-columns: repeat(3, 1fr);
|
|
|
|
|
|
gap: 20px;
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
width: 100%;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-item {
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
padding: 15px;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-title {
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image-container {
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
margin-bottom: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.image {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
height: 500px;
|
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.canvas {
|
|
|
|
|
|
position: absolute;
|
|
|
|
|
|
top: 0;
|
|
|
|
|
|
left: 0;
|
|
|
|
|
|
cursor: crosshair;
|
|
|
|
|
|
border: 1px solid #ccc;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.crop-info {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
margin-top: 5px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.button-group {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
gap: 10px;
|
|
|
|
|
|
flex-wrap: wrap;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
button {
|
|
|
|
|
|
padding: 8px 16px;
|
|
|
|
|
|
border: none;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.crop-btn {
|
|
|
|
|
|
background-color: #007bff;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.crop-btn:hover {
|
|
|
|
|
|
background-color: #0056b3;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.reset-btn {
|
|
|
|
|
|
background-color: #6c757d;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.reset-btn:hover {
|
|
|
|
|
|
background-color: #545b62;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.export-btn {
|
|
|
|
|
|
background-color: #28a745;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
padding: 12px 24px;
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
margin: 20px auto;
|
|
|
|
|
|
display: block;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.export-btn:hover {
|
|
|
|
|
|
background-color: #218838;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.upload-section {
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
margin-bottom: 30px;
|
|
|
|
|
|
padding: 20px;
|
|
|
|
|
|
background: white;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.file-input {
|
|
|
|
|
|
display: none;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.upload-label {
|
|
|
|
|
|
display: inline-block;
|
|
|
|
|
|
padding: 10px 20px;
|
|
|
|
|
|
background-color: #17a2b8;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
margin: 0 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.upload-label:hover {
|
|
|
|
|
|
background-color: #138496;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cropped-preview {
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
border: 1px solid #ddd;
|
|
|
|
|
|
padding: 5px;
|
|
|
|
|
|
background-color: #f9f9f9;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.cropped-img {
|
|
|
|
|
|
max-width: 100%;
|
|
|
|
|
|
height: 150px;
|
|
|
|
|
|
object-fit: contain;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.delete-btn {
|
|
|
|
|
|
background-color: #dc3545;
|
|
|
|
|
|
color: white;
|
|
|
|
|
|
margin-top: 10px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.delete-btn:hover {
|
|
|
|
|
|
background-color: #c82333;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.code-separator {
|
|
|
|
|
|
grid-column: 1 / -1;
|
|
|
|
|
|
border: 2px solid #007bff;
|
|
|
|
|
|
margin: 20px 0;
|
|
|
|
|
|
text-align: center;
|
|
|
|
|
|
font-weight: bold;
|
|
|
|
|
|
color: #007bff;
|
|
|
|
|
|
background-color: #f0f8ff;
|
|
|
|
|
|
padding: 10px;
|
|
|
|
|
|
border-radius: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.loading {
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
font-style: italic;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.error {
|
|
|
|
|
|
color: #dc3545;
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
}
|
|
|
|
|
|
</style>
|
|
|
|
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
|
|
<div class="container">
|
|
|
|
|
|
<div class="header">
|
|
|
|
|
|
<h1>图片裁剪工具</h1>
|
|
|
|
|
|
<p>选择图片区域进行裁剪,然后导出所有裁剪结果</p>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div id="imagesGrid" class="images-grid">
|
|
|
|
|
|
<!-- 动态生成的图片项目 -->
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<button class="export-btn" onclick="exportAllCroppedImages()">导出所有裁剪图片</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 存储裁剪后的图片数据
|
|
|
|
|
|
const croppedImages = {};
|
|
|
|
|
|
|
|
|
|
|
|
// 裁剪状态
|
|
|
|
|
|
const cropStates = {};
|
|
|
|
|
|
|
|
|
|
|
|
// 存储每个code的裁剪计数
|
|
|
|
|
|
const cropCounters = {};
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化页面
|
|
|
|
|
|
function initializePage() {
|
|
|
|
|
|
const grid = document.getElementById('imagesGrid');
|
|
|
|
|
|
grid.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
// 按code分组
|
|
|
|
|
|
const groupedData = {};
|
|
|
|
|
|
imagesData.forEach((item, index) => {
|
|
|
|
|
|
const code = item.code || `img_${index}`;
|
|
|
|
|
|
if (!groupedData[code]) {
|
|
|
|
|
|
groupedData[code] = [];
|
|
|
|
|
|
}
|
|
|
|
|
|
groupedData[code].push({ ...item, index });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 为每个code添加分隔符和图片
|
|
|
|
|
|
Object.keys(groupedData).forEach((code, codeIndex) => {
|
|
|
|
|
|
// 添加分隔符(如果不是第一个)
|
|
|
|
|
|
if (codeIndex > 0) {
|
|
|
|
|
|
const separator = document.createElement('div');
|
|
|
|
|
|
separator.className = 'code-separator';
|
|
|
|
|
|
separator.textContent = `Code: ${code}`;
|
|
|
|
|
|
grid.appendChild(separator);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 添加该code的所有图片
|
|
|
|
|
|
groupedData[code].forEach((item) => {
|
|
|
|
|
|
const key = `${item.code}_1${item.index + 1}`;
|
|
|
|
|
|
const url = item.url;
|
|
|
|
|
|
const imageItem = createImageItem(key, url, code);
|
|
|
|
|
|
grid.appendChild(imageItem);
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化裁剪计数器
|
|
|
|
|
|
if (!cropCounters[code]) {
|
|
|
|
|
|
cropCounters[code] = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 在元素添加到DOM后,添加鼠标悬停事件
|
|
|
|
|
|
const container = document.getElementById(`container-${key}`);
|
|
|
|
|
|
const img = document.getElementById(`img-${key}`);
|
|
|
|
|
|
|
|
|
|
|
|
if (container) {
|
|
|
|
|
|
container.addEventListener('mouseenter', function() {
|
|
|
|
|
|
if (!cropStates[key].cropping) {
|
|
|
|
|
|
startCrop(key);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建单个图片项目
|
|
|
|
|
|
function createImageItem(key, url, code) {
|
|
|
|
|
|
const div = document.createElement('div');
|
|
|
|
|
|
div.className = 'image-item';
|
|
|
|
|
|
div.innerHTML = `
|
|
|
|
|
|
<div class="image-title">${key}</div>
|
|
|
|
|
|
<div class="image-container" id="container-${key}">
|
|
|
|
|
|
<img class="image" id="img-${key}" src="${url}" alt="图片 ${key}" crossorigin="anonymous" onerror="handleImageError('${key}')">
|
|
|
|
|
|
<canvas class="canvas" id="canvas-${key}" style="display: none;"></canvas>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div class="crop-info" id="info-${key}">鼠标悬停开始裁剪</div>
|
|
|
|
|
|
<div class="button-group">
|
|
|
|
|
|
<button class="reset-btn" onclick="resetCrop('${key}')">重置裁剪</button>
|
|
|
|
|
|
<button class="delete-btn" onclick="deleteImage('${key}')">删除图片</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div id="cropped-preview-${key}"></div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
// 初始化裁剪状态
|
|
|
|
|
|
cropStates[key] = {
|
|
|
|
|
|
isSelecting: false,
|
|
|
|
|
|
startX: 0,
|
|
|
|
|
|
startY: 0,
|
|
|
|
|
|
endX: 0,
|
|
|
|
|
|
endY: 0,
|
|
|
|
|
|
cropping: false
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
return div;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理图片加载错误
|
|
|
|
|
|
function handleImageError(key) {
|
|
|
|
|
|
const url = "未知URL";
|
|
|
|
|
|
const container = document.getElementById(`container-${key}`);
|
|
|
|
|
|
container.innerHTML = `<div class="error">图片加载失败: ${url}</div>`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 开始裁剪
|
|
|
|
|
|
function startCrop(key) {
|
|
|
|
|
|
const img = document.getElementById(`img-${key}`);
|
|
|
|
|
|
const canvas = document.getElementById(`canvas-${key}`);
|
|
|
|
|
|
const info = document.getElementById(`info-${key}`);
|
|
|
|
|
|
|
|
|
|
|
|
if (!img.complete || img.naturalWidth === 0) {
|
|
|
|
|
|
info.textContent = '图片尚未加载完成';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 设置canvas大小和位置
|
|
|
|
|
|
canvas.width = img.clientWidth;
|
|
|
|
|
|
canvas.height = img.clientHeight;
|
|
|
|
|
|
canvas.style.display = 'block';
|
|
|
|
|
|
canvas.style.width = img.clientWidth + 'px';
|
|
|
|
|
|
canvas.style.height = img.clientHeight + 'px';
|
|
|
|
|
|
|
|
|
|
|
|
// 清除之前的裁剪
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
|
|
|
|
|
|
// 添加事件监听器
|
|
|
|
|
|
canvas.onmousedown = function(e) { startSelection(e, key); };
|
|
|
|
|
|
canvas.onmousemove = function(e) { updateSelection(e, key); };
|
|
|
|
|
|
canvas.onmouseup = function(e) { endSelection(e, key); };
|
|
|
|
|
|
canvas.onmouseleave = function(e) { endSelection(e, key); };
|
|
|
|
|
|
|
|
|
|
|
|
// 触摸事件支持
|
|
|
|
|
|
canvas.ontouchstart = function(e) { startSelection(e.touches[0], key); };
|
|
|
|
|
|
canvas.ontouchmove = function(e) { updateSelection(e.touches[0], key); };
|
|
|
|
|
|
canvas.ontouchend = function(e) { endSelection(e.changedTouches[0], key); };
|
|
|
|
|
|
|
|
|
|
|
|
cropStates[key].cropping = true;
|
|
|
|
|
|
info.textContent = '拖动鼠标选择裁剪区域';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 开始选择
|
|
|
|
|
|
function startSelection(e, key) {
|
|
|
|
|
|
const canvas = document.getElementById(`canvas-${key}`);
|
|
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
cropStates[key].isSelecting = true;
|
|
|
|
|
|
cropStates[key].startX = e.clientX - rect.left;
|
|
|
|
|
|
cropStates[key].startY = e.clientY - rect.top;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新选择
|
|
|
|
|
|
function updateSelection(e, key) {
|
|
|
|
|
|
if (!cropStates[key].isSelecting) return;
|
|
|
|
|
|
|
|
|
|
|
|
const canvas = document.getElementById(`canvas-${key}`);
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
const rect = canvas.getBoundingClientRect();
|
|
|
|
|
|
|
|
|
|
|
|
cropStates[key].endX = e.clientX - rect.left;
|
|
|
|
|
|
cropStates[key].endY = e.clientY - rect.top;
|
|
|
|
|
|
|
|
|
|
|
|
// 清除画布并绘制选择框
|
|
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
ctx.strokeStyle = '#007bff';
|
|
|
|
|
|
ctx.lineWidth = 2;
|
|
|
|
|
|
ctx.setLineDash([5, 5]);
|
|
|
|
|
|
|
|
|
|
|
|
const x = Math.min(cropStates[key].startX, cropStates[key].endX);
|
|
|
|
|
|
const y = Math.min(cropStates[key].startY, cropStates[key].endY);
|
|
|
|
|
|
const width = Math.abs(cropStates[key].endX - cropStates[key].startX);
|
|
|
|
|
|
const height = Math.abs(cropStates[key].endY - cropStates[key].startY);
|
|
|
|
|
|
|
|
|
|
|
|
ctx.strokeRect(x, y, width, height);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新信息
|
|
|
|
|
|
document.getElementById(`info-${key}`).textContent =
|
|
|
|
|
|
`选择区域: ${Math.round(x)}, ${Math.round(y)}, ${Math.round(width)}x${Math.round(height)}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 结束选择
|
|
|
|
|
|
function endSelection(e, key) {
|
|
|
|
|
|
if (!cropStates[key].isSelecting) return;
|
|
|
|
|
|
|
|
|
|
|
|
cropStates[key].isSelecting = false;
|
|
|
|
|
|
|
|
|
|
|
|
const canvas = document.getElementById(`canvas-${key}`);
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
|
|
|
|
|
|
const x = Math.min(cropStates[key].startX, cropStates[key].endX);
|
|
|
|
|
|
const y = Math.min(cropStates[key].startY, cropStates[key].endY);
|
|
|
|
|
|
const width = Math.abs(cropStates[key].endX - cropStates[key].startX);
|
|
|
|
|
|
const height = Math.abs(cropStates[key].endY - cropStates[key].startY);
|
|
|
|
|
|
|
|
|
|
|
|
if (width > 5 && height > 5) {
|
|
|
|
|
|
// 执行裁剪
|
|
|
|
|
|
performCrop(key, x, y, width, height);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
document.getElementById(`info-${key}`).textContent = '选择区域太小,请重新选择';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 执行裁剪
|
|
|
|
|
|
function performCrop(key, x, y, width, height) {
|
|
|
|
|
|
const img = document.getElementById(`img-${key}`);
|
|
|
|
|
|
const previewContainer = document.getElementById(`cropped-preview-${key}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 提取code
|
|
|
|
|
|
const code = key.split('_')[0];
|
|
|
|
|
|
|
|
|
|
|
|
// 增加该code的裁剪计数
|
|
|
|
|
|
cropCounters[code] = (cropCounters[code] || 0) + 1;
|
|
|
|
|
|
|
|
|
|
|
|
// 创建canvas进行裁剪
|
|
|
|
|
|
const cropCanvas = document.createElement('canvas');
|
|
|
|
|
|
const cropCtx = cropCanvas.getContext('2d');
|
|
|
|
|
|
|
|
|
|
|
|
// 计算实际图片坐标
|
|
|
|
|
|
const scaleX = img.naturalWidth / img.clientWidth;
|
|
|
|
|
|
const scaleY = img.naturalHeight / img.clientHeight;
|
|
|
|
|
|
|
|
|
|
|
|
const actualX = x * scaleX;
|
|
|
|
|
|
const actualY = y * scaleY;
|
|
|
|
|
|
const actualWidth = width * scaleX;
|
|
|
|
|
|
const actualHeight = height * scaleY;
|
|
|
|
|
|
|
|
|
|
|
|
cropCanvas.width = actualWidth;
|
|
|
|
|
|
cropCanvas.height = actualHeight;
|
|
|
|
|
|
|
|
|
|
|
|
// 裁剪图片
|
|
|
|
|
|
cropCtx.drawImage(img, actualX, actualY, actualWidth, actualHeight,
|
|
|
|
|
|
0, 0, actualWidth, actualHeight);
|
|
|
|
|
|
|
|
|
|
|
|
// 转换为blob并存储
|
|
|
|
|
|
cropCanvas.toBlob(function(blob) {
|
|
|
|
|
|
if (!blob) {
|
|
|
|
|
|
console.error('Failed to create blob from canvas');
|
|
|
|
|
|
document.getElementById(`info-${key}`).textContent = '裁剪失败:无法生成图片数据';
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 使用code_1 + 序号的格式,如130665_11, 130665_12
|
|
|
|
|
|
const cropKey = `${code}_1${cropCounters[code]}`;
|
|
|
|
|
|
croppedImages[cropKey] = blob;
|
|
|
|
|
|
|
|
|
|
|
|
// 显示预览
|
|
|
|
|
|
previewContainer.innerHTML = `
|
|
|
|
|
|
<div class="cropped-preview">
|
|
|
|
|
|
<div>裁剪预览 (${cropKey}):</div>
|
|
|
|
|
|
<img class="cropped-img" src="${URL.createObjectURL(blob)}" alt="裁剪结果">
|
|
|
|
|
|
</div>
|
|
|
|
|
|
`;
|
|
|
|
|
|
|
|
|
|
|
|
document.getElementById(`info-${key}`).textContent =
|
|
|
|
|
|
`裁剪完成: ${Math.round(actualWidth)}x${Math.round(actualHeight)} 像素 (${cropKey})`;
|
|
|
|
|
|
}, 'image/png');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 重置裁剪
|
|
|
|
|
|
function resetCrop(key) {
|
|
|
|
|
|
const canvas = document.getElementById(`canvas-${key}`);
|
|
|
|
|
|
const previewContainer = document.getElementById(`cropped-preview-${key}`);
|
|
|
|
|
|
const info = document.getElementById(`info-${key}`);
|
|
|
|
|
|
|
|
|
|
|
|
// 隐藏canvas
|
|
|
|
|
|
canvas.style.display = 'none';
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
|
|
|
|
|
|
// 清除预览
|
|
|
|
|
|
previewContainer.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
// 删除存储的裁剪图片
|
|
|
|
|
|
delete croppedImages[key];
|
|
|
|
|
|
|
|
|
|
|
|
// 重置状态
|
|
|
|
|
|
cropStates[key] = {
|
|
|
|
|
|
isSelecting: false,
|
|
|
|
|
|
startX: 0,
|
|
|
|
|
|
startY: 0,
|
|
|
|
|
|
endX: 0,
|
|
|
|
|
|
endY: 0,
|
|
|
|
|
|
cropping: false
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
info.textContent = '点击图片开始裁剪';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 删除图片
|
|
|
|
|
|
function deleteImage(key) {
|
|
|
|
|
|
// 从imagesData中移除
|
|
|
|
|
|
const index = imagesData.findIndex(item => {
|
|
|
|
|
|
const itemKey = item.code ? `${item.code}_1${imagesData.indexOf(item) + 1}` : `img_${imagesData.indexOf(item) + 1}`;
|
|
|
|
|
|
return itemKey === key;
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (index !== -1) {
|
|
|
|
|
|
imagesData.splice(index, 1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清除相关数据
|
|
|
|
|
|
delete croppedImages[key];
|
|
|
|
|
|
delete cropStates[key];
|
|
|
|
|
|
|
|
|
|
|
|
// 重新初始化页面
|
|
|
|
|
|
initializePage();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// 创建并下载所有裁剪图片
|
|
|
|
|
|
async function exportAllCroppedImages() {
|
|
|
|
|
|
const croppedKeys = Object.keys(croppedImages);
|
|
|
|
|
|
|
|
|
|
|
|
if (croppedKeys.length === 0) {
|
|
|
|
|
|
alert('没有裁剪的图片可以导出!');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 逐个下载图片文件
|
|
|
|
|
|
for (const key of croppedKeys) {
|
|
|
|
|
|
const blob = croppedImages[key];
|
|
|
|
|
|
const fileName = `${key}.png`;
|
|
|
|
|
|
downloadFile(blob, fileName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
alert(`已导出 ${croppedKeys.length} 个裁剪图片\n文件格式:PNG\n文件名格式:130665_11.png, 130665_12.png\n请手动创建文件夹整理`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建ZIP文件
|
|
|
|
|
|
async function createZipFile() {
|
|
|
|
|
|
const files = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 收集所有文件数据
|
|
|
|
|
|
for (const key of Object.keys(croppedImages)) {
|
|
|
|
|
|
const blob = croppedImages[key];
|
|
|
|
|
|
const arrayBuffer = await blobToArrayBuffer(blob);
|
|
|
|
|
|
const fileName = `${key}/${key}_11.png`; // 改为PNG格式
|
|
|
|
|
|
|
|
|
|
|
|
files.push({
|
|
|
|
|
|
name: fileName,
|
|
|
|
|
|
data: new Uint8Array(arrayBuffer)
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成ZIP文件
|
|
|
|
|
|
return createSimpleZip(files);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Blob转ArrayBuffer
|
|
|
|
|
|
function blobToArrayBuffer(blob) {
|
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
reader.onload = function() {
|
|
|
|
|
|
resolve(reader.result);
|
|
|
|
|
|
};
|
|
|
|
|
|
reader.readAsArrayBuffer(blob);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建BMP文件数据
|
|
|
|
|
|
async function createBMPData(pngBlob) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 创建一个canvas来转换图片格式
|
|
|
|
|
|
const img = new Image();
|
|
|
|
|
|
const canvas = document.createElement('canvas');
|
|
|
|
|
|
const ctx = canvas.getContext('2d');
|
|
|
|
|
|
|
|
|
|
|
|
// 创建一个Promise来等待图片加载
|
|
|
|
|
|
await new Promise((resolve, reject) => {
|
|
|
|
|
|
img.onload = resolve;
|
|
|
|
|
|
img.onerror = reject;
|
|
|
|
|
|
img.src = URL.createObjectURL(pngBlob);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 设置canvas尺寸
|
|
|
|
|
|
canvas.width = img.width;
|
|
|
|
|
|
canvas.height = img.height;
|
|
|
|
|
|
|
|
|
|
|
|
// 绘制图片到canvas
|
|
|
|
|
|
ctx.drawImage(img, 0, 0);
|
|
|
|
|
|
|
|
|
|
|
|
// 获取图片数据
|
|
|
|
|
|
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
|
|
|
|
|
|
const pixels = imageData.data;
|
|
|
|
|
|
|
|
|
|
|
|
const width = canvas.width;
|
|
|
|
|
|
const height = canvas.height;
|
|
|
|
|
|
const bytesPerPixel = 3; // RGB
|
|
|
|
|
|
const rowSize = Math.ceil((bytesPerPixel * width) / 4) * 4; // 行必须是4字节对齐
|
|
|
|
|
|
const pixelDataSize = rowSize * height;
|
|
|
|
|
|
const fileSize = 54 + pixelDataSize; // 54字节头部 + 像素数据
|
|
|
|
|
|
|
|
|
|
|
|
const bmpData = new Uint8Array(fileSize);
|
|
|
|
|
|
|
|
|
|
|
|
// BMP文件头 (14字节)
|
|
|
|
|
|
bmpData[0] = 0x42; bmpData[1] = 0x4D; // 'BM'
|
|
|
|
|
|
bmpData[2] = fileSize & 0xFF;
|
|
|
|
|
|
bmpData[3] = (fileSize >> 8) & 0xFF;
|
|
|
|
|
|
bmpData[4] = (fileSize >> 16) & 0xFF;
|
|
|
|
|
|
bmpData[5] = (fileSize >> 24) & 0xFF;
|
|
|
|
|
|
// 保留字段 (6-9) = 0
|
|
|
|
|
|
// 数据偏移 (10-13) = 54
|
|
|
|
|
|
bmpData[10] = 54; bmpData[11] = 0; bmpData[12] = 0; bmpData[13] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// BMP信息头 (40字节)
|
|
|
|
|
|
// 信息头大小 (14-17) = 40
|
|
|
|
|
|
bmpData[14] = 40; bmpData[15] = 0; bmpData[16] = 0; bmpData[17] = 0;
|
|
|
|
|
|
// 宽度 (18-21)
|
|
|
|
|
|
bmpData[18] = width & 0xFF;
|
|
|
|
|
|
bmpData[19] = (width >> 8) & 0xFF;
|
|
|
|
|
|
bmpData[20] = (width >> 16) & 0xFF;
|
|
|
|
|
|
bmpData[21] = (width >> 24) & 0xFF;
|
|
|
|
|
|
// 高度 (22-25)
|
|
|
|
|
|
bmpData[22] = height & 0xFF;
|
|
|
|
|
|
bmpData[23] = (height >> 8) & 0xFF;
|
|
|
|
|
|
bmpData[24] = (height >> 16) & 0xFF;
|
|
|
|
|
|
bmpData[25] = (height >> 24) & 0xFF;
|
|
|
|
|
|
// 平面数 (26-27) = 1
|
|
|
|
|
|
bmpData[26] = 1; bmpData[27] = 0;
|
|
|
|
|
|
// 位深度 (28-29) = 24
|
|
|
|
|
|
bmpData[28] = 24; bmpData[29] = 0;
|
|
|
|
|
|
// 压缩方式 (30-33) = 0 (无压缩)
|
|
|
|
|
|
// 图像大小 (34-37) = 0 (无压缩时可以为0)
|
|
|
|
|
|
// X分辨率 (38-41) = 0
|
|
|
|
|
|
// Y分辨率 (42-45) = 0
|
|
|
|
|
|
// 颜色数 (46-49) = 0
|
|
|
|
|
|
// 重要颜色数 (50-53) = 0
|
|
|
|
|
|
|
|
|
|
|
|
// 像素数据 (从54开始)
|
|
|
|
|
|
// BMP是从下到上存储的,每行4字节对齐
|
|
|
|
|
|
let pixelIndex = 54;
|
|
|
|
|
|
for (let y = height - 1; y >= 0; y--) {
|
|
|
|
|
|
for (let x = 0; x < width; x++) {
|
|
|
|
|
|
const srcIndex = (y * width + x) * 4;
|
|
|
|
|
|
// BMP是BGR格式
|
|
|
|
|
|
bmpData[pixelIndex++] = pixels[srcIndex + 2]; // B
|
|
|
|
|
|
bmpData[pixelIndex++] = pixels[srcIndex + 1]; // G
|
|
|
|
|
|
bmpData[pixelIndex++] = pixels[srcIndex + 0]; // R
|
|
|
|
|
|
}
|
|
|
|
|
|
// 行填充到4字节对齐
|
|
|
|
|
|
const padding = rowSize - (width * bytesPerPixel);
|
|
|
|
|
|
for (let p = 0; p < padding; p++) {
|
|
|
|
|
|
bmpData[pixelIndex++] = 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 清理URL
|
|
|
|
|
|
URL.revokeObjectURL(img.src);
|
|
|
|
|
|
|
|
|
|
|
|
return bmpData;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.error('BMP转换失败:', error);
|
|
|
|
|
|
// 如果转换失败,返回PNG数据(虽然扩展名是.bmp,但至少有数据)
|
|
|
|
|
|
return new Uint8Array(await pngBlob.arrayBuffer());
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 创建完整的ZIP文件
|
|
|
|
|
|
function createSimpleZip(files) {
|
|
|
|
|
|
const encoder = new TextEncoder();
|
|
|
|
|
|
let zipData = new Uint8Array(0);
|
|
|
|
|
|
const centralDirectory = [];
|
|
|
|
|
|
let offset = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 添加所有文件的本地文件头和数据
|
|
|
|
|
|
for (const file of files) {
|
|
|
|
|
|
const fileNameBytes = encoder.encode(file.name);
|
|
|
|
|
|
const fileData = file.data;
|
|
|
|
|
|
|
|
|
|
|
|
// ZIP本地文件头 (30字节 + 文件名长度 + 文件数据)
|
|
|
|
|
|
const localFileHeader = new Uint8Array(30 + fileNameBytes.length);
|
|
|
|
|
|
|
|
|
|
|
|
// 本地文件头签名
|
|
|
|
|
|
localFileHeader[0] = 0x50; localFileHeader[1] = 0x4B; localFileHeader[2] = 0x03; localFileHeader[3] = 0x04;
|
|
|
|
|
|
|
|
|
|
|
|
// 版本 (20)
|
|
|
|
|
|
localFileHeader[4] = 20; localFileHeader[5] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 标志
|
|
|
|
|
|
localFileHeader[6] = 0; localFileHeader[7] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 压缩方法 (0 = 无压缩)
|
|
|
|
|
|
localFileHeader[8] = 0; localFileHeader[9] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 修改时间和日期
|
|
|
|
|
|
const now = new Date();
|
|
|
|
|
|
const dosTime = ((now.getHours() << 11) | (now.getMinutes() << 5) | (now.getSeconds() / 2)) & 0xFFFF;
|
|
|
|
|
|
const dosDate = (((now.getFullYear() - 1980) << 9) | ((now.getMonth() + 1) << 5) | now.getDate()) & 0xFFFF;
|
|
|
|
|
|
localFileHeader[10] = dosTime & 0xFF;
|
|
|
|
|
|
localFileHeader[11] = (dosTime >> 8) & 0xFF;
|
|
|
|
|
|
localFileHeader[12] = dosDate & 0xFF;
|
|
|
|
|
|
localFileHeader[13] = (dosDate >> 8) & 0xFF;
|
|
|
|
|
|
|
|
|
|
|
|
// CRC32 (简化为0)
|
|
|
|
|
|
localFileHeader[14] = 0; localFileHeader[15] = 0; localFileHeader[16] = 0; localFileHeader[17] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 压缩大小
|
|
|
|
|
|
const compressedSize = fileData.length;
|
|
|
|
|
|
localFileHeader[18] = compressedSize & 0xFF;
|
|
|
|
|
|
localFileHeader[19] = (compressedSize >> 8) & 0xFF;
|
|
|
|
|
|
localFileHeader[20] = (compressedSize >> 16) & 0xFF;
|
|
|
|
|
|
localFileHeader[21] = (compressedSize >> 24) & 0xFF;
|
|
|
|
|
|
|
|
|
|
|
|
// 未压缩大小
|
|
|
|
|
|
localFileHeader[22] = compressedSize & 0xFF;
|
|
|
|
|
|
localFileHeader[23] = (compressedSize >> 8) & 0xFF;
|
|
|
|
|
|
localFileHeader[24] = (compressedSize >> 16) & 0xFF;
|
|
|
|
|
|
localFileHeader[25] = (compressedSize >> 24) & 0xFF;
|
|
|
|
|
|
|
|
|
|
|
|
// 文件名长度
|
|
|
|
|
|
const fileNameLength = fileNameBytes.length;
|
|
|
|
|
|
localFileHeader[26] = fileNameLength & 0xFF;
|
|
|
|
|
|
localFileHeader[27] = (fileNameLength >> 8) & 0xFF;
|
|
|
|
|
|
|
|
|
|
|
|
// 额外字段长度
|
|
|
|
|
|
localFileHeader[28] = 0; localFileHeader[29] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 文件名
|
|
|
|
|
|
localFileHeader.set(fileNameBytes, 30);
|
|
|
|
|
|
|
|
|
|
|
|
// 记录中央目录信息
|
|
|
|
|
|
centralDirectory.push({
|
|
|
|
|
|
fileName: file.name,
|
|
|
|
|
|
fileNameBytes: fileNameBytes,
|
|
|
|
|
|
fileData: fileData,
|
|
|
|
|
|
offset: offset,
|
|
|
|
|
|
compressedSize: compressedSize,
|
|
|
|
|
|
uncompressedSize: compressedSize,
|
|
|
|
|
|
crc32: 0,
|
|
|
|
|
|
modTime: dosTime,
|
|
|
|
|
|
modDate: dosDate
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 合并本地文件头和数据
|
|
|
|
|
|
const fileContent = new Uint8Array(localFileHeader.length + fileData.length);
|
|
|
|
|
|
fileContent.set(localFileHeader);
|
|
|
|
|
|
fileContent.set(fileData, localFileHeader.length);
|
|
|
|
|
|
|
|
|
|
|
|
// 添加到ZIP数据
|
|
|
|
|
|
const newZipData = new Uint8Array(zipData.length + fileContent.length);
|
|
|
|
|
|
newZipData.set(zipData);
|
|
|
|
|
|
newZipData.set(fileContent, zipData.length);
|
|
|
|
|
|
zipData = newZipData;
|
|
|
|
|
|
|
|
|
|
|
|
offset += fileContent.length;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 2. 添加中央目录
|
|
|
|
|
|
const centralDirStart = offset;
|
|
|
|
|
|
let centralDirData = new Uint8Array(0);
|
|
|
|
|
|
|
|
|
|
|
|
for (const entry of centralDirectory) {
|
|
|
|
|
|
// 中央目录文件头 (46字节 + 文件名长度)
|
|
|
|
|
|
const centralHeader = new Uint8Array(46 + entry.fileNameBytes.length);
|
|
|
|
|
|
|
|
|
|
|
|
// 中央目录签名
|
|
|
|
|
|
centralHeader[0] = 0x50; centralHeader[1] = 0x4B; centralHeader[2] = 0x01; centralHeader[3] = 0x02;
|
|
|
|
|
|
|
|
|
|
|
|
// 版本 made by (20)
|
|
|
|
|
|
centralHeader[4] = 20; centralHeader[5] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 版本 needed to extract (20)
|
|
|
|
|
|
centralHeader[6] = 20; centralHeader[7] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 标志
|
|
|
|
|
|
centralHeader[8] = 0; centralHeader[9] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 压缩方法
|
|
|
|
|
|
centralHeader[10] = 0; centralHeader[11] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 修改时间
|
|
|
|
|
|
centralHeader[12] = entry.modTime & 0xFF;
|
|
|
|
|
|
centralHeader[13] = (entry.modTime >> 8) & 0xFF;
|
|
|
|
|
|
centralHeader[14] = entry.modDate & 0xFF;
|
|
|
|
|
|
centralHeader[15] = (entry.modDate >> 8) & 0xFF;
|
|
|
|
|
|
|
|
|
|
|
|
// CRC32
|
|
|
|
|
|
centralHeader[16] = entry.crc32 & 0xFF;
|
|
|
|
|
|
centralHeader[17] = (entry.crc32 >> 8) & 0xFF;
|
|
|
|
|
|
centralHeader[18] = (entry.crc32 >> 16) & 0xFF;
|
|
|
|
|
|
centralHeader[19] = (entry.crc32 >> 24) & 0xFF;
|
|
|
|
|
|
|
|
|
|
|
|
// 压缩大小
|
|
|
|
|
|
centralHeader[20] = entry.compressedSize & 0xFF;
|
|
|
|
|
|
centralHeader[21] = (entry.compressedSize >> 8) & 0xFF;
|
|
|
|
|
|
centralHeader[22] = (entry.compressedSize >> 16) & 0xFF;
|
|
|
|
|
|
centralHeader[23] = (entry.compressedSize >> 24) & 0xFF;
|
|
|
|
|
|
|
|
|
|
|
|
// 未压缩大小
|
|
|
|
|
|
centralHeader[24] = entry.uncompressedSize & 0xFF;
|
|
|
|
|
|
centralHeader[25] = (entry.uncompressedSize >> 8) & 0xFF;
|
|
|
|
|
|
centralHeader[26] = (entry.uncompressedSize >> 16) & 0xFF;
|
|
|
|
|
|
centralHeader[27] = (entry.uncompressedSize >> 24) & 0xFF;
|
|
|
|
|
|
|
|
|
|
|
|
// 文件名长度
|
|
|
|
|
|
const fileNameLength = entry.fileNameBytes.length;
|
|
|
|
|
|
centralHeader[28] = fileNameLength & 0xFF;
|
|
|
|
|
|
centralHeader[29] = (fileNameLength >> 8) & 0xFF;
|
|
|
|
|
|
|
|
|
|
|
|
// 额外字段长度
|
|
|
|
|
|
centralHeader[30] = 0; centralHeader[31] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 文件注释长度
|
|
|
|
|
|
centralHeader[32] = 0; centralHeader[33] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 磁盘号
|
|
|
|
|
|
centralHeader[34] = 0; centralHeader[35] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 内部文件属性
|
|
|
|
|
|
centralHeader[36] = 0; centralHeader[37] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 外部文件属性
|
|
|
|
|
|
centralHeader[38] = 0; centralHeader[39] = 0; centralHeader[40] = 0; centralHeader[41] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 本地文件头偏移
|
|
|
|
|
|
centralHeader[42] = entry.offset & 0xFF;
|
|
|
|
|
|
centralHeader[43] = (entry.offset >> 8) & 0xFF;
|
|
|
|
|
|
centralHeader[44] = (entry.offset >> 16) & 0xFF;
|
|
|
|
|
|
centralHeader[45] = (entry.offset >> 24) & 0xFF;
|
|
|
|
|
|
|
|
|
|
|
|
// 文件名
|
|
|
|
|
|
centralHeader.set(entry.fileNameBytes, 46);
|
|
|
|
|
|
|
|
|
|
|
|
// 添加到中央目录数据
|
|
|
|
|
|
const newCentralDirData = new Uint8Array(centralDirData.length + centralHeader.length);
|
|
|
|
|
|
newCentralDirData.set(centralDirData);
|
|
|
|
|
|
newCentralDirData.set(centralHeader, centralDirData.length);
|
|
|
|
|
|
centralDirData = newCentralDirData;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 3. 添加中央目录结束记录
|
|
|
|
|
|
const centralDirSize = centralDirData.length;
|
|
|
|
|
|
const totalEntries = centralDirectory.length;
|
|
|
|
|
|
|
|
|
|
|
|
// ZIP结束记录 (22字节)
|
|
|
|
|
|
const endRecord = new Uint8Array(22);
|
|
|
|
|
|
|
|
|
|
|
|
// 结束记录签名
|
|
|
|
|
|
endRecord[0] = 0x50; endRecord[1] = 0x4B; endRecord[2] = 0x05; endRecord[3] = 0x06;
|
|
|
|
|
|
|
|
|
|
|
|
// 磁盘号
|
|
|
|
|
|
endRecord[4] = 0; endRecord[5] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 中央目录磁盘号
|
|
|
|
|
|
endRecord[6] = 0; endRecord[7] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 本磁盘记录数
|
|
|
|
|
|
endRecord[8] = totalEntries & 0xFF;
|
|
|
|
|
|
endRecord[9] = (totalEntries >> 8) & 0xFF;
|
|
|
|
|
|
|
|
|
|
|
|
// 总记录数
|
|
|
|
|
|
endRecord[10] = totalEntries & 0xFF;
|
|
|
|
|
|
endRecord[11] = (totalEntries >> 8) & 0xFF;
|
|
|
|
|
|
|
|
|
|
|
|
// 中央目录大小
|
|
|
|
|
|
endRecord[12] = centralDirSize & 0xFF;
|
|
|
|
|
|
endRecord[13] = (centralDirSize >> 8) & 0xFF;
|
|
|
|
|
|
endRecord[14] = (centralDirSize >> 16) & 0xFF;
|
|
|
|
|
|
endRecord[15] = (centralDirSize >> 24) & 0xFF;
|
|
|
|
|
|
|
|
|
|
|
|
// 中央目录偏移
|
|
|
|
|
|
endRecord[16] = centralDirStart & 0xFF;
|
|
|
|
|
|
endRecord[17] = (centralDirStart >> 8) & 0xFF;
|
|
|
|
|
|
endRecord[18] = (centralDirStart >> 16) & 0xFF;
|
|
|
|
|
|
endRecord[19] = (centralDirStart >> 24) & 0xFF;
|
|
|
|
|
|
|
|
|
|
|
|
// ZIP文件注释长度
|
|
|
|
|
|
endRecord[20] = 0; endRecord[21] = 0;
|
|
|
|
|
|
|
|
|
|
|
|
// 合并所有数据
|
|
|
|
|
|
const finalZipData = new Uint8Array(zipData.length + centralDirData.length + endRecord.length);
|
|
|
|
|
|
finalZipData.set(zipData);
|
|
|
|
|
|
finalZipData.set(centralDirData, zipData.length);
|
|
|
|
|
|
finalZipData.set(endRecord, zipData.length + centralDirData.length);
|
|
|
|
|
|
|
|
|
|
|
|
return new Blob([finalZipData], { type: 'application/zip' });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 下载文件
|
|
|
|
|
|
function downloadFile(blob, fileName) {
|
|
|
|
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
|
|
|
|
|
|
|
|
const a = document.createElement('a');
|
|
|
|
|
|
a.href = url;
|
|
|
|
|
|
a.download = fileName;
|
|
|
|
|
|
document.body.appendChild(a);
|
|
|
|
|
|
a.click();
|
|
|
|
|
|
document.body.removeChild(a);
|
|
|
|
|
|
|
|
|
|
|
|
URL.revokeObjectURL(url);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理文件上传
|
|
|
|
|
|
function handleFileUpload(event) {
|
|
|
|
|
|
const files = event.target.files;
|
|
|
|
|
|
const grid = document.getElementById('imagesGrid');
|
|
|
|
|
|
grid.innerHTML = '';
|
|
|
|
|
|
|
|
|
|
|
|
// 清空之前的数据
|
|
|
|
|
|
Object.keys(croppedImages).forEach(key => delete croppedImages[key]);
|
|
|
|
|
|
Object.keys(cropStates).forEach(key => delete cropStates[key]);
|
|
|
|
|
|
|
|
|
|
|
|
Array.from(files).forEach((file, index) => {
|
|
|
|
|
|
const key = String(index + 1);
|
|
|
|
|
|
const reader = new FileReader();
|
|
|
|
|
|
|
|
|
|
|
|
reader.onload = function(e) {
|
|
|
|
|
|
imagesData[key] = e.target.result;
|
|
|
|
|
|
const imageItem = createImageItem(key, e.target.result);
|
|
|
|
|
|
grid.appendChild(imageItem);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
reader.readAsDataURL(file);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 加载默认示例图片
|
|
|
|
|
|
function loadDefaultImages() {
|
|
|
|
|
|
// 使用一些在线的示例图片
|
|
|
|
|
|
const sampleImages = {
|
|
|
|
|
|
"1": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZGRkIi8+CiAgPHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxOCIgZmlsbD0iIzk5OSIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuWbvueJh+iAheWbvTwvdGV4dD4KICA8Y2lyY2xlIGN4PSI3NSIgY3k9IjUwIiByPSIyMCIgZmlsbD0iIzY2NiIvPgogIDxjaXJjbGUgY3g9IjIyNSIgY3k9IjUwIiByPSIyMCIgZmlsbD0iIzk5OSIvPgogIDxyZWN0IHg9IjUwIiB5PSIxMDAiIHdpZHRoPSIyMDAiIGhlaWdodD0iNTAiIGZpbGw9IiNjY2MiLz4KPC9zdmc+",
|
|
|
|
|
|
"2": "data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzAwIiBoZWlnaHQ9IjIwMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KICA8cmVjdCB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIiBmaWxsPSIjZGRmIi8+CiAgPHRleHQgeD0iNTAlIiB5PSI1MCUiIGZvbnQtZmFtaWx5PSJBcmlhbCIgZm9udC1zaXplPSIxOCIgZmlsbD0iIzY2NiIgdGV4dC1hbmNob3I9Im1pZGRsZSIgZHk9Ii4zZW0iPuS4lueVjOWbvueJhzwvdGV4dD4KICA8cG9seWdvbiBwb2ludHM9IjE1MCwzMCAxODAsMTAwIDEyMCwxMDAiIGZpbGw9IiM0NGQiLz4KICA8cmVjdCB4PSI1MCIgeT0iMTIwIiB3aWR0aD0iNjAiIGhlaWdodD0iNDAiIGZpbGw9IiNlZWUiLz4KICA8cmVjdCB4PSIxOTAiIHk9IjEyMCIgd2lkdGg9IjYwIiBoZWlnaHQ9IjQwIiBmaWxsPSIjZWVlIi8+Cjwvc3ZnPg=="
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 清空之前的数据
|
|
|
|
|
|
Object.keys(croppedImages).forEach(key => delete croppedImages[key]);
|
|
|
|
|
|
Object.keys(cropStates).forEach(key => delete cropStates[key]);
|
|
|
|
|
|
|
|
|
|
|
|
// 更新imagesData
|
|
|
|
|
|
Object.assign(imagesData, sampleImages);
|
|
|
|
|
|
|
|
|
|
|
|
// 重新初始化页面
|
|
|
|
|
|
initializePage();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 页面加载完成后初始化
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', initializePage);
|
|
|
|
|
|
// 图片数据对象
|
|
|
|
|
|
const imagesData = [
|
|
|
|
|
|
{"code": "130665", "url": "https://fuss10.elemecdn.com/e/5d/4a731a90594a4af544c0c25941171jpeg.jpeg"}
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
</script>
|
|
|
|
|
|
</body>
|
|
|
|
|
|
</html>
|