diff --git a/src/app/page.tsx b/src/app/page.tsx index 1afe10e..f3a853e 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -18,7 +18,7 @@ import { // 导入新的类型和组件 import { GridDownloadOptions } from '../types/downloadTypes'; import DownloadSettingsModal, { gridLineColorOptions } from '../components/DownloadSettingsModal'; -import { downloadImage } from '../utils/imageDownloader'; +import { downloadImage, importCsvData } from '../utils/imageDownloader'; import { colorSystemOptions, @@ -138,7 +138,8 @@ export default function Home() { gridInterval: 10, showCoordinates: true, gridLineColor: gridLineColorOptions[0].value, - includeStats: true // 默认包含统计信息 + includeStats: true, // 默认包含统计信息 + exportCsv: false // 默认不导出CSV }); // 新增:高亮相关状态 @@ -147,6 +148,9 @@ export default function Home() { // 新增:完整色板切换状态 const [showFullPalette, setShowFullPalette] = useState(false); + // 添加标记来区分是否是CSV导入的图像 + const [isFromCsvImport, setIsFromCsvImport] = useState(false); + // 新增:颜色替换相关状态 const [colorReplaceState, setColorReplaceState] = useState<{ isActive: boolean; @@ -434,12 +438,13 @@ export default function Home() { // 更严格的文件类型检查 const allowedTypes = ['image/jpeg', 'image/jpg', 'image/png']; const fileType = file.type.toLowerCase(); + const fileName = file.name.toLowerCase(); - if (allowedTypes.includes(fileType) || file.type.startsWith('image/')) { + if (allowedTypes.includes(fileType) || file.type.startsWith('image/') || fileName.endsWith('.csv')) { setExcludedColorKeys(new Set()); // ++ 重置排除列表 ++ processFile(file); } else { - alert(`不支持的文件类型: ${file.type || '未知'}。请拖放 JPG 或 PNG 格式的图片文件。`); + alert(`不支持的文件类型: ${file.type || '未知'}。请拖放 JPG、PNG 格式的图片文件,或 CSV 数据文件。`); } } } catch (error) { @@ -453,32 +458,131 @@ export default function Home() { event.stopPropagation(); }; - const processFile = (file: File) => { - const reader = new FileReader(); - reader.onload = (e) => { - const result = e.target?.result as string; - setOriginalImageSrc(result); - setMappedPixelData(null); - setGridDimensions(null); - setColorCounts(null); - setTotalBeadCount(0); - setInitialGridColorKeys(new Set()); // ++ 重置初始键 ++ - // ++ 重置横轴格子数量为默认值 ++ - const defaultGranularity = 100; - setGranularity(defaultGranularity); - setGranularityInput(defaultGranularity.toString()); - setRemapTrigger(prev => prev + 1); // Trigger full remap for new image - }; - reader.onerror = () => { - console.error("文件读取失败"); - alert("无法读取文件。"); - setInitialGridColorKeys(new Set()); // ++ 重置初始键 ++ + // 根据mappedPixelData生成合成的originalImageSrc + const generateSyntheticImageFromPixelData = (pixelData: MappedPixel[][], dimensions: { N: number; M: number }): string => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + + if (!ctx) { + console.error('无法创建canvas上下文'); + return ''; + } + + // 设置画布尺寸,每个像素用8x8像素来表示以确保清晰度 + const pixelSize = 8; + canvas.width = dimensions.N * pixelSize; + canvas.height = dimensions.M * pixelSize; + + // 绘制每个像素 + pixelData.forEach((row, rowIndex) => { + row.forEach((cell, colIndex) => { + if (cell) { + // 使用颜色,外部单元格用白色 + const color = cell.isExternal ? '#FFFFFF' : cell.color; + ctx.fillStyle = color; + ctx.fillRect( + colIndex * pixelSize, + rowIndex * pixelSize, + pixelSize, + pixelSize + ); + } + }); + }); + + // 转换为dataURL + return canvas.toDataURL('image/png'); + }; + + const processFile = (file: File) => { + // 检查文件类型 + const fileExtension = file.name.split('.').pop()?.toLowerCase(); + + if (fileExtension === 'csv') { + // 处理CSV文件 + console.log('正在导入CSV文件...'); + importCsvData(file) + .then(({ mappedPixelData, gridDimensions }) => { + console.log(`成功导入CSV文件: ${gridDimensions.N}x${gridDimensions.M}`); + + // 设置导入的数据 + setMappedPixelData(mappedPixelData); + setGridDimensions(gridDimensions); + setOriginalImageSrc(null); // CSV导入时没有原始图片 + + // 计算颜色统计 + const colorCountsMap: { [key: string]: { count: number; color: string } } = {}; + let totalCount = 0; + + mappedPixelData.forEach(row => { + row.forEach(cell => { + if (cell && !cell.isExternal) { + const colorKey = cell.color.toUpperCase(); + if (colorCountsMap[colorKey]) { + colorCountsMap[colorKey].count++; + } else { + colorCountsMap[colorKey] = { + count: 1, + color: cell.color + }; + } + totalCount++; + } + }); + }); + + setColorCounts(colorCountsMap); + setTotalBeadCount(totalCount); + setInitialGridColorKeys(new Set(Object.keys(colorCountsMap))); + + // 根据mappedPixelData生成合成的originalImageSrc + const syntheticImageSrc = generateSyntheticImageFromPixelData(mappedPixelData, gridDimensions); + + setOriginalImageSrc(syntheticImageSrc); + + // 重置状态 + setIsManualColoringMode(false); + setSelectedColor(null); + setIsEraseMode(false); + + // 设置格子数量为导入的尺寸,避免重新映射时尺寸被修改 + setGranularity(gridDimensions.N); + setGranularityInput(gridDimensions.N.toString()); + + alert(`成功导入CSV文件!图纸尺寸:${gridDimensions.N}x${gridDimensions.M},共使用${Object.keys(colorCountsMap).length}种颜色。`); + }) + .catch(error => { + console.error('CSV导入失败:', error); + alert(`CSV导入失败:${error.message}`); + }); + } else { + // 处理图片文件 + const reader = new FileReader(); + reader.onload = (e) => { + const result = e.target?.result as string; + setOriginalImageSrc(result); + setMappedPixelData(null); + setGridDimensions(null); + setColorCounts(null); + setTotalBeadCount(0); + setInitialGridColorKeys(new Set()); // ++ 重置初始键 ++ + // ++ 重置横轴格子数量为默认值 ++ + const defaultGranularity = 100; + setGranularity(defaultGranularity); + setGranularityInput(defaultGranularity.toString()); + setRemapTrigger(prev => prev + 1); // Trigger full remap for new image + }; + reader.onerror = () => { + console.error("文件读取失败"); + alert("无法读取文件。"); + setInitialGridColorKeys(new Set()); // ++ 重置初始键 ++ + } + reader.readAsDataURL(file); + // ++ Reset manual coloring mode when a new file is processed ++ + setIsManualColoringMode(false); + setSelectedColor(null); + setIsEraseMode(false); } - reader.readAsDataURL(file); - // ++ Reset manual coloring mode when a new file is processed ++ - setIsManualColoringMode(false); - setSelectedColor(null); - setIsEraseMode(false); }; // 处理一键擦除模式切换 @@ -1734,7 +1838,7 @@ export default function Home() { {/* Text color */}

拖放图片到此处,或点击选择文件

{/* Text color */} -

支持 JPG, PNG 格式

+

支持 JPG, PNG 图片格式,或 CSV 数据文件

{/* Apply dark mode styles to the Tip Box */} @@ -1751,7 +1855,7 @@ export default function Home() { )} - + {/* Controls and Output Area */} {originalImageSrc && ( diff --git a/src/components/DownloadSettingsModal.tsx b/src/components/DownloadSettingsModal.tsx index f16d6ad..cac9ff8 100644 --- a/src/components/DownloadSettingsModal.tsx +++ b/src/components/DownloadSettingsModal.tsx @@ -167,6 +167,27 @@ const DownloadSettingsModal: React.FC = ({
+ + {/* 新增: 导出CSV hex数据选项 */} +
+
+ + + 导出hex颜色值的CSV文件,可用于重新导入 + +
+ +
diff --git a/src/types/downloadTypes.ts b/src/types/downloadTypes.ts index 716fb23..74c0eaf 100644 --- a/src/types/downloadTypes.ts +++ b/src/types/downloadTypes.ts @@ -5,4 +5,5 @@ export type GridDownloadOptions = { showCoordinates: boolean; gridLineColor: string; includeStats: boolean; + exportCsv: boolean; // 新增:是否同时导出CSV hex数据 }; \ No newline at end of file diff --git a/src/utils/imageDownloader.ts b/src/utils/imageDownloader.ts index 2c145dd..70ac4e7 100644 --- a/src/utils/imageDownloader.ts +++ b/src/utils/imageDownloader.ts @@ -46,6 +46,160 @@ function sortColorKeys(a: string, b: string): number { return a.localeCompare(b); } +// 导出CSV hex数据的函数 +export function exportCsvData({ + mappedPixelData, + gridDimensions, + selectedColorSystem +}: { + mappedPixelData: MappedPixel[][] | null; + gridDimensions: { N: number; M: number } | null; + selectedColorSystem: ColorSystem; +}): void { + if (!mappedPixelData || !gridDimensions) { + console.error("导出失败: 映射数据或尺寸无效。"); + alert("无法导出CSV,数据未生成或无效。"); + return; + } + + const { N, M } = gridDimensions; + + // 生成CSV内容,每行代表图纸的一行 + const csvLines: string[] = []; + + for (let row = 0; row < M; row++) { + const rowData: string[] = []; + for (let col = 0; col < N; col++) { + const cellData = mappedPixelData[row][col]; + if (cellData && !cellData.isExternal) { + // 内部单元格,记录hex颜色值 + rowData.push(cellData.color); + } else { + // 外部单元格或空白,使用特殊标记 + rowData.push('TRANSPARENT'); + } + } + csvLines.push(rowData.join(',')); + } + + // 创建CSV内容 + const csvContent = csvLines.join('\n'); + + // 创建并下载CSV文件 + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + + link.setAttribute('href', url); + link.setAttribute('download', `bead-pattern-${N}x${M}-${selectedColorSystem}.csv`); + link.style.visibility = 'hidden'; + + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + // 释放URL对象 + URL.revokeObjectURL(url); + + console.log("CSV数据导出完成"); +} + +// 导入CSV hex数据的函数 +export function importCsvData(file: File): Promise<{ + mappedPixelData: MappedPixel[][]; + gridDimensions: { N: number; M: number }; +}> { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + + reader.onload = (e) => { + try { + const text = e.target?.result as string; + if (!text) { + reject(new Error('无法读取文件内容')); + return; + } + + // 解析CSV内容 + const lines = text.trim().split('\n'); + const M = lines.length; // 行数 + + if (M === 0) { + reject(new Error('CSV文件为空')); + return; + } + + // 解析第一行获取列数 + const firstRowData = lines[0].split(','); + const N = firstRowData.length; // 列数 + + if (N === 0) { + reject(new Error('CSV文件格式无效')); + return; + } + + // 创建映射数据 + const mappedPixelData: MappedPixel[][] = []; + + for (let row = 0; row < M; row++) { + const rowData = lines[row].split(','); + const mappedRow: MappedPixel[] = []; + + // 确保每行都有正确的列数 + if (rowData.length !== N) { + reject(new Error(`第${row + 1}行的列数不匹配,期望${N}列,实际${rowData.length}列`)); + return; + } + + for (let col = 0; col < N; col++) { + const cellValue = rowData[col].trim(); + + if (cellValue === 'TRANSPARENT' || cellValue === '') { + // 外部/透明单元格 + mappedRow.push({ + key: 'TRANSPARENT', + color: '#FFFFFF', + isExternal: true + }); + } else { + // 验证hex颜色格式 + const hexPattern = /^#[0-9A-Fa-f]{6}$/; + if (!hexPattern.test(cellValue)) { + reject(new Error(`第${row + 1}行第${col + 1}列的颜色值无效:${cellValue}`)); + return; + } + + // 内部单元格 + mappedRow.push({ + key: cellValue.toUpperCase(), + color: cellValue.toUpperCase(), + isExternal: false + }); + } + } + + mappedPixelData.push(mappedRow); + } + + // 返回解析结果 + resolve({ + mappedPixelData, + gridDimensions: { N, M } + }); + + } catch (error) { + reject(new Error(`解析CSV文件失败:${error}`)); + } + }; + + reader.onerror = () => { + reject(new Error('读取文件失败')); + }; + + reader.readAsText(file, 'utf-8'); + }); +} + // 下载图片的主函数 export async function downloadImage({ mappedPixelData, @@ -666,6 +820,15 @@ export async function downloadImage({ link.click(); document.body.removeChild(link); console.log("Grid image download initiated."); + + // 如果启用了CSV导出,同时导出CSV文件 + if (options.exportCsv) { + exportCsvData({ + mappedPixelData, + gridDimensions, + selectedColorSystem + }); + } } catch (e) { console.error("下载图纸失败:", e); alert("无法生成图纸下载链接。");