Skip to content

Instantly share code, notes, and snippets.

@CrazyCoder
Last active November 30, 2025 14:53
Show Gist options
  • Select an option

  • Save CrazyCoder/31f02014a1d569986c7b9940e775bb5d to your computer and use it in GitHub Desktop.

Select an option

Save CrazyCoder/31f02014a1d569986c7b9940e775bb5d to your computer and use it in GitHub Desktop.
Batch XTH file generator for Xteink X4
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>批量XTH文件生成器</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
line-height: 1.6;
color: #333;
background-color: #f5f7fa;
padding: 20px;
}
.container {
max-width: 1400px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
padding: 25px;
}
h1 {
text-align: center;
margin-bottom: 25px;
color: #2c3e50;
}
.drop-zone {
border: 3px dashed #3498db;
border-radius: 10px;
padding: 40px;
text-align: center;
background-color: #f8f9fa;
margin-bottom: 25px;
transition: all 0.3s;
cursor: pointer;
}
.drop-zone:hover, .drop-zone.drag-over {
border-color: #2980b9;
background-color: #e8f4f8;
}
.drop-zone p {
font-size: 18px;
color: #7f8c8d;
margin-bottom: 10px;
}
.drop-zone .hint {
font-size: 14px;
color: #95a5a6;
}
.file-input {
display: none;
}
.controls {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px;
margin-bottom: 25px;
}
.control-group {
background-color: #f8f9fa;
padding: 15px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
}
.control-group h3 {
margin-bottom: 15px;
color: #34495e;
}
.slider-container {
margin-bottom: 15px;
}
.slider-container label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.slider-value {
display: inline-block;
width: 50px;
text-align: right;
}
input[type="range"] {
width: 100%;
}
.options-group {
display: flex;
gap: 15px;
margin-top: 10px;
flex-wrap: wrap;
}
.option-label {
display: flex;
align-items: center;
gap: 5px;
}
.image-list {
margin-bottom: 25px;
}
.image-list h3 {
margin-bottom: 15px;
color: #34495e;
}
.image-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 15px;
}
.image-item {
background: #f8f9fa;
border-radius: 8px;
padding: 10px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.05);
position: relative;
}
.image-item canvas {
width: 100%;
height: auto;
border: 1px solid #ddd;
border-radius: 5px;
background: white;
}
.image-item .image-name {
margin-top: 8px;
font-size: 12px;
color: #7f8c8d;
word-break: break-all;
}
.image-item .image-status {
position: absolute;
top: 15px;
right: 15px;
padding: 5px 10px;
border-radius: 5px;
font-size: 12px;
font-weight: bold;
}
.image-item .status-processing {
background-color: #f39c12;
color: white;
}
.image-item .status-ready {
background-color: #2ecc71;
color: white;
}
.image-item .status-error {
background-color: #e74c3c;
color: white;
}
.action-buttons {
display: flex;
justify-content: center;
gap: 15px;
flex-wrap: wrap;
margin-bottom: 25px;
}
.action-btn {
padding: 12px 25px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
transition: all 0.3s;
}
.process-btn {
background-color: #3498db;
color: white;
}
.process-btn:hover:not(:disabled) {
background-color: #2980b9;
}
.download-btn {
background-color: #2ecc71;
color: white;
}
.download-btn:hover:not(:disabled) {
background-color: #27ae60;
}
.clear-btn {
background-color: #e74c3c;
color: white;
}
.clear-btn:hover:not(:disabled) {
background-color: #c0392b;
}
.action-btn:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
.progress-section {
margin-bottom: 25px;
display: none;
}
.progress-section.active {
display: block;
}
.progress-bar {
width: 100%;
height: 30px;
background-color: #ecf0f1;
border-radius: 15px;
overflow: hidden;
margin-bottom: 10px;
}
.progress-fill {
height: 100%;
background-color: #3498db;
transition: width 0.3s;
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: bold;
}
.info-text {
text-align: center;
color: #7f8c8d;
font-style: italic;
}
</style>
</head>
<body>
<div class="container">
<h1>批量XTH文件生成器</h1>
<div class="drop-zone" id="dropZone">
<p>拖放图片到这里,或点击选择文件</p>
<p class="hint">支持批量选择,自动裁剪为 480×800 像素</p>
<input type="file" id="fileInput" class="file-input" multiple accept="image/*">
</div>
<div class="controls">
<div class="control-group">
<h3>4灰阶设置</h3>
<div class="slider-container">
<label>阈值1: <span class="slider-value" id="threshold1Value">85</span></label>
<input type="range" id="threshold1Slider" min="0" max="255" value="85">
</div>
<div class="slider-container">
<label>阈值2: <span class="slider-value" id="threshold2Value">170</span></label>
<input type="range" id="threshold2Slider" min="0" max="255" value="170">
</div>
<div class="slider-container">
<label>阈值3: <span class="slider-value" id="threshold3Value">255</span></label>
<input type="range" id="threshold3Slider" min="0" max="255" value="255">
</div>
</div>
<div class="control-group">
<h3>图像处理选项</h3>
<div class="options-group">
<label class="option-label">
<input type="checkbox" id="invertColors">
<span>反色处理</span>
</label>
<label class="option-label">
<input type="checkbox" id="enableDithering" checked>
<span>启用抖动</span>
</label>
</div>
<div class="slider-container">
<label>抖动强度: <span class="slider-value" id="ditherStrengthValue">80</span></label>
<input type="range" id="ditherStrengthSlider" min="0" max="100" value="80">
</div>
</div>
</div>
<div class="image-list">
<h3>图片列表 (<span id="imageCount">0</span>)</h3>
<div class="image-grid" id="imageGrid"></div>
</div>
<div class="progress-section" id="progressSection">
<div class="progress-bar">
<div class="progress-fill" id="progressFill" style="width: 0%">0%</div>
</div>
<p class="info-text" id="progressText">准备处理...</p>
</div>
<div class="action-buttons">
<button class="action-btn process-btn" id="processBtn" disabled>处理所有图片</button>
<button class="action-btn download-btn" id="downloadBtn" disabled>下载所有XTH文件</button>
<button class="action-btn clear-btn" id="clearBtn">清空列表</button>
</div>
<p class="info-text">所有图片将自动裁剪为 480×800 像素并转换为XTH格式</p>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const TARGET_WIDTH = 480;
const TARGET_HEIGHT = 800;
// 获取DOM元素
const dropZone = document.getElementById('dropZone');
const fileInput = document.getElementById('fileInput');
const imageGrid = document.getElementById('imageGrid');
const imageCount = document.getElementById('imageCount');
const threshold1Slider = document.getElementById('threshold1Slider');
const threshold1Value = document.getElementById('threshold1Value');
const threshold2Slider = document.getElementById('threshold2Slider');
const threshold2Value = document.getElementById('threshold2Value');
const threshold3Slider = document.getElementById('threshold3Slider');
const threshold3Value = document.getElementById('threshold3Value');
const invertColors = document.getElementById('invertColors');
const enableDithering = document.getElementById('enableDithering');
const ditherStrengthSlider = document.getElementById('ditherStrengthSlider');
const ditherStrengthValue = document.getElementById('ditherStrengthValue');
const processBtn = document.getElementById('processBtn');
const downloadBtn = document.getElementById('downloadBtn');
const clearBtn = document.getElementById('clearBtn');
const progressSection = document.getElementById('progressSection');
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
// 图片列表
let imageList = [];
// 当前设置
let currentSettings = {
threshold1: 85,
threshold2: 170,
threshold3: 255,
invertColors: false,
enableDithering: true,
ditherStrength: 80
};
// 滑块事件监听
threshold1Slider.addEventListener('input', () => {
threshold1Value.textContent = threshold1Slider.value;
currentSettings.threshold1 = parseInt(threshold1Slider.value);
});
threshold2Slider.addEventListener('input', () => {
threshold2Value.textContent = threshold2Slider.value;
currentSettings.threshold2 = parseInt(threshold2Slider.value);
});
threshold3Slider.addEventListener('input', () => {
threshold3Value.textContent = threshold3Slider.value;
currentSettings.threshold3 = parseInt(threshold3Slider.value);
});
ditherStrengthSlider.addEventListener('input', () => {
ditherStrengthValue.textContent = ditherStrengthSlider.value;
currentSettings.ditherStrength = parseInt(ditherStrengthSlider.value);
});
invertColors.addEventListener('change', () => {
currentSettings.invertColors = invertColors.checked;
});
enableDithering.addEventListener('change', () => {
currentSettings.enableDithering = enableDithering.checked;
});
// 拖放区域事件
dropZone.addEventListener('click', () => {
fileInput.click();
});
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.classList.add('drag-over');
});
dropZone.addEventListener('dragleave', () => {
dropZone.classList.remove('drag-over');
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.classList.remove('drag-over');
const files = Array.from(e.dataTransfer.files).filter(file => file.type.startsWith('image/'));
handleFiles(files);
});
fileInput.addEventListener('change', (e) => {
const files = Array.from(e.target.files);
handleFiles(files);
});
// 处理文件
function handleFiles(files) {
files.forEach(file => {
const reader = new FileReader();
reader.onload = (event) => {
const img = new Image();
img.onload = () => {
imageList.push({
file: file,
image: img,
name: file.name,
status: 'pending',
canvas: null,
xthData: null
});
updateImageList();
};
img.src = event.target.result;
};
reader.readAsDataURL(file);
});
}
// 更新图片列表显示
function updateImageList() {
imageGrid.innerHTML = '';
imageCount.textContent = imageList.length;
imageList.forEach((item, index) => {
const itemDiv = document.createElement('div');
itemDiv.className = 'image-item';
const canvas = document.createElement('canvas');
canvas.width = 200;
canvas.height = Math.round(200 * TARGET_HEIGHT / TARGET_WIDTH);
const ctx = canvas.getContext('2d');
// 绘制缩略图
const scale = Math.min(canvas.width / item.image.width, canvas.height / item.image.height);
const x = (canvas.width - item.image.width * scale) / 2;
const y = (canvas.height - item.image.height * scale) / 2;
ctx.drawImage(item.image, x, y, item.image.width * scale, item.image.height * scale);
const statusDiv = document.createElement('div');
statusDiv.className = 'image-status';
updateStatus(statusDiv, item.status);
const nameDiv = document.createElement('div');
nameDiv.className = 'image-name';
nameDiv.textContent = item.name;
itemDiv.appendChild(canvas);
itemDiv.appendChild(statusDiv);
itemDiv.appendChild(nameDiv);
imageGrid.appendChild(itemDiv);
item.canvas = canvas;
item.statusDiv = statusDiv;
});
processBtn.disabled = imageList.length === 0;
}
// 更新状态显示
function updateStatus(statusDiv, status) {
statusDiv.className = 'image-status';
if (status === 'processing') {
statusDiv.textContent = '处理中';
statusDiv.classList.add('status-processing');
} else if (status === 'ready') {
statusDiv.textContent = '就绪';
statusDiv.classList.add('status-ready');
} else if (status === 'error') {
statusDiv.textContent = '错误';
statusDiv.classList.add('status-error');
} else {
statusDiv.textContent = '待处理';
}
}
// 裁剪图片到目标尺寸(fit裁剪)
function cropImageToSize(image, targetWidth, targetHeight) {
const canvas = document.createElement('canvas');
canvas.width = targetWidth;
canvas.height = targetHeight;
const ctx = canvas.getContext('2d');
// 计算缩放比例,保持宽高比
const scale = Math.max(targetWidth / image.width, targetHeight / image.height);
const scaledWidth = image.width * scale;
const scaledHeight = image.height * scale;
// 居中裁剪
const x = (targetWidth - scaledWidth) / 2;
const y = (targetHeight - scaledHeight) / 2;
ctx.fillStyle = 'white';
ctx.fillRect(0, 0, targetWidth, targetHeight);
ctx.drawImage(image, x, y, scaledWidth, scaledHeight);
return canvas;
}
// 生成4灰阶图像
function generate4Grayscale(canvas, settings) {
const width = canvas.width;
const height = canvas.height;
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const grayscaleImageData = ctx.createImageData(width, height);
const grayscaleData = grayscaleImageData.data;
let errorMatrix = null;
if (settings.enableDithering) {
errorMatrix = new Array(width * height).fill(0);
}
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const index = (y * width + x) * 4;
const gray = Math.round(0.299 * data[index] + 0.587 * data[index+1] + 0.114 * data[index+2]);
let adjustedGray = gray;
if (settings.enableDithering) {
const errorIndex = y * width + x;
adjustedGray += errorMatrix[errorIndex] * (settings.ditherStrength / 100);
adjustedGray = Math.max(0, Math.min(255, adjustedGray));
}
let grayscaleValue;
if (adjustedGray < settings.threshold1) {
grayscaleValue = 0;
} else if (adjustedGray < settings.threshold2) {
grayscaleValue = 85;
} else if (adjustedGray < settings.threshold3) {
grayscaleValue = 170;
} else {
grayscaleValue = 255;
}
if (settings.enableDithering) {
const error = adjustedGray - grayscaleValue;
if (x < width - 1) {
errorMatrix[y * width + x + 1] += error * 7/16;
}
if (y < height - 1) {
if (x > 0) {
errorMatrix[(y + 1) * width + x - 1] += error * 3/16;
}
errorMatrix[(y + 1) * width + x] += error * 5/16;
if (x < width - 1) {
errorMatrix[(y + 1) * width + x + 1] += error * 1/16;
}
}
}
if (settings.invertColors) {
grayscaleValue = 255 - grayscaleValue;
}
grayscaleData[index] = grayscaleValue;
grayscaleData[index+1] = grayscaleValue;
grayscaleData[index+2] = grayscaleValue;
grayscaleData[index+3] = data[index+3];
}
}
ctx.putImageData(grayscaleImageData, 0, 0);
}
// 生成XTH文件数据
function generateXthData(canvas, settings) {
const width = canvas.width;
const height = canvas.height;
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, width, height);
const data = imageData.data;
const arrayData1 = [];
const arrayData2 = [];
// 垂直扫描:从右到左处理列
for (let x = width - 1; x >= 0; x--) {
for (let y = 0; y < height; y += 8) {
let byte1 = 0;
let byte2 = 0;
for (let i = 0; i < 8; i++) {
if (y + i < height) {
const index = ((y + i) * width + x) * 4;
const grayValue = data[index];
let twoBitValue;
if (grayValue < settings.threshold1) {
twoBitValue = 0;
} else if (grayValue < settings.threshold2) {
twoBitValue = 2;
} else if (grayValue < settings.threshold3) {
twoBitValue = 1;
} else {
twoBitValue = 3;
}
twoBitValue = 3 - twoBitValue;
const bit1 = (twoBitValue >> 1) & 1;
const bit2 = twoBitValue & 1;
byte1 |= bit1 << (7 - i);
byte2 |= bit2 << (7 - i);
}
}
arrayData1.push(byte1);
arrayData2.push(byte2);
}
}
return createXthFile(width, height, arrayData1, arrayData2);
}
// 创建XTH文件
function createXthFile(width, height, data1, data2) {
const headerSize = 22;
const dataSize = data1.length + data2.length;
const buffer = new ArrayBuffer(headerSize + dataSize);
const view = new DataView(buffer);
view.setUint32(0, 0x58544800, false);
view.setUint16(4, width, true);
view.setUint16(6, height, true);
view.setUint8(8, 0);
view.setUint8(9, 0);
view.setUint32(10, dataSize, true);
let checksum = 0;
for (let i = 0; i < data1.length; i++) {
checksum += data1[i];
}
for (let i = 0; i < data2.length; i++) {
checksum += data2[i];
}
view.setBigUint64(14, BigInt(checksum), true);
const dataArray = new Uint8Array(buffer, headerSize);
dataArray.set(data1, 0);
dataArray.set(data2, data1.length);
return buffer;
}
// 处理所有图片
processBtn.addEventListener('click', async () => {
if (imageList.length === 0) return;
processBtn.disabled = true;
downloadBtn.disabled = true;
progressSection.classList.add('active');
let processed = 0;
const total = imageList.length;
for (let i = 0; i < imageList.length; i++) {
const item = imageList[i];
item.status = 'processing';
if (item.statusDiv) {
updateStatus(item.statusDiv, 'processing');
}
try {
// 裁剪图片
const croppedCanvas = cropImageToSize(item.image, TARGET_WIDTH, TARGET_HEIGHT);
// 生成4灰阶
generate4Grayscale(croppedCanvas, currentSettings);
// 生成XTH数据
item.xthData = generateXthData(croppedCanvas, currentSettings);
item.status = 'ready';
// 更新缩略图显示处理后的图像
if (item.canvas) {
const thumbCtx = item.canvas.getContext('2d');
thumbCtx.clearRect(0, 0, item.canvas.width, item.canvas.height);
thumbCtx.drawImage(croppedCanvas, 0, 0, item.canvas.width, item.canvas.height);
}
} catch (error) {
console.error('处理图片失败:', item.name, error);
item.status = 'error';
}
if (item.statusDiv) {
updateStatus(item.statusDiv, item.status);
}
processed++;
const progress = Math.round((processed / total) * 100);
progressFill.style.width = progress + '%';
progressFill.textContent = progress + '%';
progressText.textContent = `已处理 ${processed} / ${total} 张图片`;
// 让UI更新
await new Promise(resolve => setTimeout(resolve, 10));
}
processBtn.disabled = false;
downloadBtn.disabled = imageList.filter(item => item.status === 'ready').length === 0;
progressText.textContent = `处理完成!共 ${imageList.filter(item => item.status === 'ready').length} 张图片已就绪`;
});
// 下载所有XTH文件
downloadBtn.addEventListener('click', () => {
const readyItems = imageList.filter(item => item.status === 'ready' && item.xthData);
if (readyItems.length === 0) {
alert('没有可下载的文件');
return;
}
readyItems.forEach(item => {
const blob = new Blob([item.xthData], { type: 'application/octet-stream' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
const fileName = item.name.substring(0, item.name.lastIndexOf('.')) + '.xth';
a.download = fileName;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
}, 100);
});
});
// 清空列表
clearBtn.addEventListener('click', () => {
imageList = [];
updateImageList();
progressSection.classList.remove('active');
downloadBtn.disabled = true;
});
});
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment