Spaces:
Running
Running
| // Global State Management | |
| const state = { | |
| currentFile: null, | |
| fileType: null, | |
| pdfDoc: null, | |
| currentPage: 1, | |
| totalPages: 0, | |
| extractedData: [], | |
| extractedDataWithCoords: [], | |
| isProcessing: false, | |
| ocrWorker: null, | |
| abortController: null | |
| }; | |
| // Initialize PDF.js Worker | |
| pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js'; | |
| // DOM Elements | |
| let elements = {}; | |
| function initializeElements() { | |
| elements = { | |
| uploadSection: document.getElementById('upload-section'), | |
| previewSection: document.getElementById('preview-section'), | |
| resultsSection: document.getElementById('results-section'), | |
| previewContainer: document.getElementById('preview-container'), | |
| pageSelector: document.getElementById('page-selector'), | |
| fileInfo: document.getElementById('file-info'), | |
| resultsGrid: document.getElementById('results-grid'), | |
| resultsStats: document.getElementById('results-stats'), | |
| processingModal: document.getElementById('processing-modal'), | |
| processingTitle: document.getElementById('processing-title'), | |
| processingStatus: document.getElementById('processing-status'), | |
| progressBar: document.getElementById('progress-bar'), | |
| progressPercent: document.getElementById('progress-percent'), | |
| jsonContent: document.getElementById('json-content'), | |
| jsonPreview: document.getElementById('json-preview'), | |
| jsonChevron: document.getElementById('json-chevron'), | |
| toggleJson: document.getElementById('toggle-json'), | |
| copyAllBtn: document.getElementById('copy-all-btn'), | |
| toast: document.getElementById('toast'), | |
| toastMessage: document.getElementById('toast-message') | |
| }; | |
| } | |
| // Event Listeners Setup | |
| document.addEventListener('DOMContentLoaded', () => { | |
| initializeElements(); | |
| setupEventListeners(); | |
| setupGlobalErrorHandling(); | |
| // Listen for invalid-file event from upload-zone | |
| document.addEventListener('invalid-file', (e) => { | |
| showToast(e.detail.message, 'error'); | |
| }); | |
| }); | |
| function setupEventListeners() { | |
| // Listen for custom events from components | |
| document.addEventListener('file-selected', handleFileSelected); | |
| document.addEventListener('reset-app', resetApplication); | |
| document.addEventListener('analyze-pdf', analyzePDFStructure); | |
| document.addEventListener('ocr-thai', performOCR); | |
| document.addEventListener('export-json', () => exportData('json')); | |
| document.addEventListener('export-csv', () => exportData('csv')); | |
| document.addEventListener('export-excel', () => exportData('excel')); | |
| document.addEventListener('export-html', () => exportData('html')); | |
| document.addEventListener('copy-all', copyAllData); | |
| // Page selector change | |
| if (elements.pageSelector) { | |
| elements.pageSelector.addEventListener('change', (e) => { | |
| state.currentPage = parseInt(e.target.value); | |
| renderPDFPage(state.currentPage); | |
| }); | |
| } | |
| // JSON Toggle | |
| if (elements.toggleJson) { | |
| elements.toggleJson.addEventListener('click', toggleJsonPreview); | |
| } | |
| // Copy All Button | |
| if (elements.copyAllBtn) { | |
| elements.copyAllBtn.addEventListener('click', copyAllData); | |
| } | |
| } | |
| function setupGlobalErrorHandling() { | |
| window.addEventListener('error', (e) => { | |
| console.error('Global error:', e.error); | |
| showToast('เกิดข้อผิดพลาด: ' + (e.error?.message || 'Unknown error'), 'error'); | |
| }); | |
| window.addEventListener('unhandledrejection', (e) => { | |
| console.error('Unhandled promise rejection:', e.reason); | |
| showToast('เกิดข้อผิดพลาดในการประมวลผล', 'error'); | |
| }); | |
| } | |
| // File Handling Functions | |
| function handleFileSelected(e) { | |
| const file = e.detail.file; | |
| if (!file) return; | |
| // Validate file type | |
| const validTypes = ['application/pdf', 'image/jpeg', 'image/jpg', 'image/png']; | |
| if (!validTypes.includes(file.type)) { | |
| showToast('ไฟล์ไม่รองรับ กรุณาอัปโหลด PDF, JPG หรือ PNG', 'error'); | |
| return; | |
| } | |
| // Check file size (limit to 50MB) | |
| const maxSize = 50 * 1024 * 1024; // 50MB | |
| if (file.size > maxSize) { | |
| showToast('ไฟล์ใหญ่เกินไป กรุณาอัปโหลดไฟล์ขนาดไม่เกิน 50MB', 'error'); | |
| return; | |
| } | |
| // Clean up previous state | |
| if (state.ocrWorker) { | |
| state.ocrWorker.terminate(); | |
| state.ocrWorker = null; | |
| } | |
| if (state.abortController) { | |
| state.abortController.abort(); | |
| } | |
| state.abortController = new AbortController(); | |
| state.currentFile = file; | |
| state.extractedData = []; | |
| if (file.type === 'application/pdf') { | |
| state.fileType = 'pdf'; | |
| loadPDF(file); | |
| } else { | |
| state.fileType = 'image'; | |
| loadImage(file); | |
| } | |
| updateUIForFileLoaded(); | |
| } | |
| function updateUIForFileLoaded() { | |
| elements.uploadSection.classList.add('hidden'); | |
| elements.previewSection.classList.remove('hidden'); | |
| elements.resultsSection.classList.add('hidden'); | |
| const sizeMB = (state.currentFile.size / 1024 / 1024).toFixed(2); | |
| elements.fileInfo.textContent = `${state.currentFile.name} (${sizeMB} MB)`; | |
| } | |
| async function loadPDF(file) { | |
| try { | |
| const arrayBuffer = await file.arrayBuffer(); | |
| const pdf = await pdfjsLib.getDocument({ | |
| data: arrayBuffer, | |
| disableAutoFetch: true, | |
| disableStream: true | |
| }).promise; | |
| state.pdfDoc = pdf; | |
| state.totalPages = pdf.numPages; | |
| state.currentPage = 1; | |
| // Setup page selector | |
| setupPageSelector(); | |
| // Render first page | |
| await renderPDFPage(1); | |
| showToast('โหลด PDF สำเร็จ'); | |
| } catch (error) { | |
| console.error('PDF Load Error:', error); | |
| if (error.name !== 'AbortError') { | |
| showToast('ไม่สามารถโหลด PDF ได้', 'error'); | |
| } | |
| } | |
| } | |
| function setupPageSelector() { | |
| if (elements.pageSelector) { | |
| elements.pageSelector.innerHTML = ''; | |
| elements.pageSelector.classList.remove('hidden'); | |
| for (let i = 1; i <= state.totalPages; i++) { | |
| const option = document.createElement('option'); | |
| option.value = i; | |
| option.textContent = `หน้า ${i} จาก ${state.totalPages}`; | |
| elements.pageSelector.appendChild(option); | |
| } | |
| elements.pageSelector.value = 1; | |
| } | |
| } | |
| async function renderPDFPage(pageNum) { | |
| try { | |
| if (elements.previewContainer) { | |
| elements.previewContainer.innerHTML = '<div class="flex items-center justify-center h-64"><i data-feather="loader" class="animate-spin w-8 h-8 text-blue-600"></i></div>'; | |
| feather.replace(); | |
| } | |
| const page = await state.pdfDoc.getPage(pageNum); | |
| const scale = 1.5; | |
| const viewport = page.getViewport({ scale }); | |
| const canvas = document.createElement('canvas'); | |
| canvas.id = 'pdf-canvas'; | |
| const context = canvas.getContext('2d'); | |
| canvas.height = viewport.height; | |
| canvas.width = viewport.width; | |
| await page.render({ | |
| canvasContext: context, | |
| viewport: viewport | |
| }).promise; | |
| if (elements.previewContainer) { | |
| elements.previewContainer.innerHTML = ''; | |
| elements.previewContainer.appendChild(canvas); | |
| } | |
| page.cleanup(); | |
| } catch (error) { | |
| console.error('Render Error:', error); | |
| if (error.name !== 'AbortError') { | |
| showToast('ไม่สามารถแสดงหน้าได้', 'error'); | |
| } | |
| } | |
| } | |
| function loadImage(file) { | |
| if (elements.pageSelector) { | |
| elements.pageSelector.classList.add('hidden'); | |
| } | |
| const reader = new FileReader(); | |
| reader.onload = (e) => { | |
| const img = document.createElement('img'); | |
| img.id = 'preview-image'; | |
| img.src = e.target.result; | |
| img.className = 'max-w-full h-auto rounded-lg shadow-lg'; | |
| img.loading = 'lazy'; | |
| if (elements.previewContainer) { | |
| elements.previewContainer.innerHTML = ''; | |
| elements.previewContainer.appendChild(img); | |
| } | |
| }; | |
| reader.onerror = () => { | |
| showToast('ไม่สามารถโหลดภาพได้', 'error'); | |
| }; | |
| reader.readAsDataURL(file); | |
| } | |
| // PDF Structure Analysis | |
| async function analyzePDFStructure() { | |
| if (!state.pdfDoc || state.isProcessing) return; | |
| state.isProcessing = true; | |
| showProcessingModal('กำลังวิเคราะห์โครงสร้าง PDF', 'กำลังอ่านข้อมูลจากเอกสาร...', 0); | |
| try { | |
| const allData = []; | |
| const allDataWithCoords = []; | |
| const Y_TOLERANCE = 10; // pixels | |
| const X_GAP_THRESHOLD = 50; // pixels for column detection | |
| for (let pageNum = 1; pageNum <= state.totalPages; pageNum++) { | |
| // Check for abort signal | |
| if (state.abortController?.signal.aborted) { | |
| break; | |
| } | |
| updateProcessingProgress(`กำลังวิเคราะห์หน้า ${pageNum}/${state.totalPages}...`, ((pageNum - 1) / state.totalPages) * 100); | |
| const page = await state.pdfDoc.getPage(pageNum); | |
| const viewport = page.getViewport({ scale: 1.0 }); | |
| const textContent = await page.getTextContent(); | |
| const items = textContent.items; | |
| if (items.length === 0) { | |
| page.cleanup(); | |
| continue; | |
| } | |
| // Sort by Y position (descending - top to bottom) | |
| items.sort((a, b) => b.transform[5] - a.transform[5]); | |
| // Group by rows using Y-tolerance | |
| const rows = []; | |
| items.forEach(item => { | |
| const y = item.transform[5]; | |
| const existingRow = rows.find(r => Math.abs(r.y - y) < Y_TOLERANCE); | |
| if (existingRow) { | |
| existingRow.items.push(item); | |
| // Update average Y | |
| const totalY = existingRow.items.reduce((sum, i) => sum + i.transform[5], 0); | |
| existingRow.y = totalY / existingRow.items.length; | |
| } else { | |
| rows.push({ y, items: [item], originalY: y }); | |
| } | |
| }); | |
| // Sort rows by Y (top to bottom) | |
| rows.sort((a, b) => b.y - a.y); | |
| // Sort items in each row by X (left to right) | |
| rows.forEach(row => { | |
| row.items.sort((a, b) => a.transform[4] - b.transform[4]); | |
| }); | |
| // Detect column boundaries | |
| const columnBoundaries = detectColumnBoundaries(rows, X_GAP_THRESHOLD); | |
| // Convert to 2D array | |
| const pageData = rows.map(row => { | |
| const rowData = new Array(columnBoundaries.length + 1).fill(''); | |
| const rowDataWithCoords = new Array(columnBoundaries.length + 1).fill(null); | |
| row.items.forEach(item => { | |
| const x = item.transform[4]; | |
| const colIndex = findColumnIndex(x, columnBoundaries); | |
| if (colIndex !== -1) { | |
| const text = item.str.trim(); | |
| if (text) { | |
| rowData[colIndex] += (rowData[colIndex] ? ' ' : '') + text; | |
| // Store with coordinates | |
| if (!rowDataWithCoords[colIndex]) { | |
| rowDataWithCoords[colIndex] = { | |
| text: text, | |
| coordinates: { | |
| x: Math.round(item.transform[4]), | |
| y: Math.round(viewport.height - item.transform[5]), | |
| width: Math.round(item.width), | |
| height: Math.round(item.height) | |
| } | |
| }; | |
| } else { | |
| // Append text and update coordinates | |
| rowDataWithCoords[colIndex].text += ' ' + text; | |
| // Update coordinates to encompass the entire text | |
| rowDataWithCoords[colIndex].coordinates.width = Math.round( | |
| item.transform[4] + item.width - rowDataWithCoords[colIndex].coordinates.x | |
| ); | |
| } | |
| } | |
| } | |
| }); | |
| return { | |
| textData: rowData.filter(cell => cell.trim() !== ''), | |
| coordData: rowDataWithCoords.filter(cell => cell && cell.text.trim() !== '') | |
| }; | |
| }); | |
| // Add to main arrays | |
| pageData.forEach(row => { | |
| if (row.textData.length > 0) { | |
| allData.push(row.textData); | |
| } | |
| if (row.coordData.length > 0) { | |
| allDataWithCoords.push({ | |
| page: pageNum, | |
| cells: row.coordData | |
| }); | |
| } | |
| }); | |
| page.cleanup(); | |
| // Yield to UI thread every page | |
| await new Promise(resolve => setTimeout(resolve, 0)); | |
| } | |
| if (!state.abortController?.signal.aborted) { | |
| state.extractedData = allData; | |
| state.extractedDataWithCoords = allDataWithCoords; | |
| renderResults(); | |
| showToast(`วิเคราะห์สำเร็จ พบ ${allData.length} แถวข้อมูล`); | |
| } | |
| } catch (error) { | |
| console.error('Analysis Error:', error); | |
| if (error.name !== 'AbortError') { | |
| showToast('เกิดข้อผิดพลาดในการวิเคราะห์: ' + error.message, 'error'); | |
| } | |
| } finally { | |
| state.isProcessing = false; | |
| hideProcessingModal(); | |
| } | |
| } | |
| function detectColumnBoundaries(rows, threshold) { | |
| const gaps = []; | |
| rows.forEach(row => { | |
| for (let i = 1; i < row.items.length; i++) { | |
| const prevX = row.items[i-1].transform[4] + (row.items[i-1].width || 0); | |
| const currentX = row.items[i].transform[4]; | |
| const gap = currentX - prevX; | |
| if (gap > threshold) { | |
| gaps.push((prevX + currentX) / 2); | |
| } | |
| } | |
| }); | |
| // Cluster similar boundaries (within 20px) | |
| const clustered = []; | |
| gaps.forEach(gap => { | |
| const existing = clustered.find(c => Math.abs(c - gap) < 20); | |
| if (!existing) clustered.push(gap); | |
| }); | |
| return clustered.sort((a, b) => a - b); | |
| } | |
| function findColumnIndex(x, boundaries) { | |
| for (let i = 0; i < boundaries.length; i++) { | |
| if (x < boundaries[i]) return i; | |
| } | |
| return boundaries.length; | |
| } | |
| // OCR Functions | |
| async function performOCR() { | |
| if (state.isProcessing) return; | |
| let imageSource = null; | |
| if (state.fileType === 'image') { | |
| const img = document.getElementById('preview-image'); | |
| if (img) imageSource = img.src; | |
| } else if (state.fileType === 'pdf') { | |
| const canvas = document.getElementById('pdf-canvas'); | |
| if (canvas) imageSource = canvas.toDataURL('image/png'); | |
| } | |
| if (!imageSource) { | |
| showToast('ไม่พบเอกสารสำหรับ OCR', 'error'); | |
| return; | |
| } | |
| state.isProcessing = true; | |
| showProcessingModal('กำลังตรวจจับข้อความ (OCR)', 'กำลังโหลด Engine...', 0); | |
| try { | |
| const worker = await Tesseract.createWorker('tha', 1, { | |
| logger: m => { | |
| if (m.status === 'recognizing text') { | |
| updateProcessingProgress(`กำลังอ่านข้อความภาษาไทย... ${Math.round(m.progress * 100)}%`, m.progress * 100); | |
| } else if (m.status === 'loading language traineddata') { | |
| updateProcessingProgress('กำลังโหลดข้อมูลภาษาไทย...', 10); | |
| } | |
| }, | |
| errorHandler: err => console.error('OCR Error:', err) | |
| }); | |
| state.ocrWorker = worker; | |
| const result = await worker.recognize(imageSource); | |
| // Parse OCR result into structured data | |
| const lines = result.data.text.split('\n').filter(line => line.trim()); | |
| const parsed = lines.map(line => { | |
| // Split by multiple spaces to detect columns | |
| return line.split(/\s{2,}/).map(cell => cell.trim()).filter(cell => cell); | |
| }).filter(row => row.length > 0); | |
| state.extractedData = parsed; | |
| renderResults(); | |
| showToast(`OCR สำเร็จ พบข้อความ ${lines.length} บรรทัด`); | |
| } catch (error) { | |
| console.error('OCR Error:', error); | |
| if (error.name !== 'AbortError') { | |
| showToast('เกิดข้อผิดพลาดในการ OCR: ' + error.message, 'error'); | |
| } | |
| } finally { | |
| if (state.ocrWorker) { | |
| await state.ocrWorker.terminate(); | |
| state.ocrWorker = null; | |
| } | |
| state.isProcessing = false; | |
| hideProcessingModal(); | |
| } | |
| } | |
| // Results Rendering | |
| function renderResults() { | |
| if ((!state.extractedData.length && !state.extractedDataWithCoords.length) || !elements.resultsSection) return; | |
| elements.resultsSection.classList.remove('hidden'); | |
| elements.resultsGrid.innerHTML = ''; | |
| const totalRows = state.extractedData.length || state.extractedDataWithCoords.length; | |
| if (elements.resultsStats) { | |
| elements.resultsStats.textContent = `พบข้อมูล ${totalRows} แถว`; | |
| } | |
| // Update JSON preview with coordinate data if available | |
| const dataToShow = state.extractedDataWithCoords.length ? state.extractedDataWithCoords : state.extractedData; | |
| if (elements.jsonContent) { | |
| elements.jsonContent.textContent = JSON.stringify(dataToShow, null, 2); | |
| } | |
| const fragment = document.createDocumentFragment(); | |
| const batchSize = 50; | |
| let renderedCount = 0; | |
| function renderBatch(startIndex) { | |
| const endIndex = Math.min(startIndex + batchSize, totalRows); | |
| for (let i = startIndex; i < endIndex; i++) { | |
| const row = dataToShow[i]; | |
| const card = document.createElement('div'); | |
| card.className = 'data-card bg-white rounded-xl p-4 shadow-sm hover:shadow-md transition-all'; | |
| const header = document.createElement('div'); | |
| header.className = 'flex justify-between items-center mb-3 pb-2 border-b border-slate-100'; | |
| const rowNum = document.createElement('span'); | |
| rowNum.className = 'text-xs font-bold text-blue-600 bg-blue-50 px-2 py-1 rounded'; | |
| rowNum.textContent = `แถวที่ ${i + 1}`; | |
| const copyBtn = document.createElement('button'); | |
| copyBtn.className = 'text-slate-400 hover:text-blue-600 transition-colors'; | |
| copyBtn.innerHTML = '<i data-feather="copy" class="w-4 h-4"></i>'; | |
| copyBtn.onclick = () => copyRow(row, i); | |
| header.appendChild(rowNum); | |
| header.appendChild(copyBtn); | |
| const content = document.createElement('div'); | |
| content.className = 'space-y-2'; | |
| // Check if we have coordinate data | |
| const hasCoords = row.coordinates || (Array.isArray(row) && row.some(cell => cell && typeof cell === 'object' && cell.text)); | |
| if (hasCoords) { | |
| // Handle coordinate-based data | |
| const cells = Array.isArray(row) ? row : row.cells || []; | |
| cells.forEach((cell, colIndex) => { | |
| if (!cell) return; | |
| const cellText = typeof cell === 'object' ? cell.text : cell; | |
| const cellCoords = typeof cell === 'object' ? cell.coordinates : null; | |
| const cellDiv = document.createElement('div'); | |
| cellDiv.className = 'text-sm text-slate-700 flex gap-2 items-start'; | |
| let coordInfo = ''; | |
| if (cellCoords) { | |
| coordInfo = `<span class="text-xs text-slate-400 font-mono">[${cellCoords.x},${cellCoords.y}]</span>`; | |
| } | |
| cellDiv.innerHTML = ` | |
| <span class="text-xs text-slate-400 font-mono w-6 shrink-0">C${colIndex + 1}</span> | |
| <div class="flex-1"> | |
| <div class="flex justify-between items-start"> | |
| <span>${escapeHtml(cellText || '')}</span> | |
| ${coordInfo} | |
| </div> | |
| </div> | |
| `; | |
| content.appendChild(cellDiv); | |
| }); | |
| } else { | |
| // Handle regular array data | |
| row.forEach((cell, colIndex) => { | |
| if (!cell) return; | |
| const cellDiv = document.createElement('div'); | |
| cellDiv.className = 'text-sm text-slate-700 flex gap-2'; | |
| cellDiv.innerHTML = ` | |
| <span class="text-xs text-slate-400 font-mono w-6">C${colIndex + 1}</span> | |
| <span class="flex-1">${escapeHtml(cell)}</span> | |
| `; | |
| content.appendChild(cellDiv); | |
| }); | |
| } | |
| card.appendChild(header); | |
| card.appendChild(content); | |
| fragment.appendChild(card); | |
| } | |
| renderedCount = endIndex; | |
| if (renderedCount < totalRows) { | |
| setTimeout(() => renderBatch(renderedCount), 0); | |
| } else { | |
| elements.resultsGrid.appendChild(fragment); | |
| feather.replace(); | |
| // Scroll to results | |
| setTimeout(() => { | |
| elements.resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| }, 100); | |
| } | |
| } | |
| renderBatch(0); | |
| } | |
| // Export Functions | |
| function exportData(format) { | |
| const dataToExport = state.extractedDataWithCoords.length ? state.extractedDataWithCoords : state.extractedData; | |
| if (!dataToExport.length) { | |
| showToast('ไม่มีข้อมูลสำหรับ Export', 'error'); | |
| return; | |
| } | |
| const timestamp = new Date().toISOString().slice(0, 19).replace(/:/g, '-'); | |
| const filename = `DocuAnalyza-${timestamp}`; | |
| switch(format) { | |
| case 'json': | |
| exportJSON(filename, dataToExport); | |
| break; | |
| case 'csv': | |
| exportCSV(filename, dataToExport); | |
| break; | |
| case 'excel': | |
| exportExcel(filename, dataToExport); | |
| break; | |
| case 'html': | |
| exportHTML(filename, dataToExport); | |
| break; | |
| } | |
| } | |
| function exportJSON(filename, data) { | |
| const dataStr = JSON.stringify(data, null, 2); | |
| downloadFile(dataStr, `${filename}.json`, 'application/json'); | |
| showToast('ดาวน์โหลด JSON สำเร็จ'); | |
| } | |
| function exportCSV(filename, data) { | |
| let csv = ''; | |
| if (state.extractedDataWithCoords.length) { | |
| // Export coordinate data as CSV | |
| data.forEach((row, rowIndex) => { | |
| if (row.cells && Array.isArray(row.cells)) { | |
| const rowData = row.cells.map(cell => { | |
| if (cell && typeof cell === 'object') { | |
| return `"${(cell.text || '').toString().replace(/"/g, '""')}"`; | |
| } | |
| return '""'; | |
| }); | |
| csv += rowData.join(',') + '\n'; | |
| } | |
| }); | |
| } else { | |
| // Export regular array data | |
| csv = data.map(row => | |
| row.map(cell => `"${(cell || '').toString().replace(/"/g, '""')}"`).join(',') | |
| ).join('\n'); | |
| } | |
| downloadFile(csv, `${filename}.csv`, 'text/csv'); | |
| showToast('ดาวน์โหลด CSV สำเร็จ'); | |
| } | |
| function exportExcel(filename, data) { | |
| let worksheetData; | |
| if (state.extractedDataWithCoords.length) { | |
| // Export coordinate data | |
| worksheetData = data.map(row => { | |
| if (row.cells && Array.isArray(row.cells)) { | |
| return row.cells.map(cell => { | |
| if (cell && typeof cell === 'object') { | |
| return cell.text || ''; | |
| } | |
| return ''; | |
| }); | |
| } | |
| return []; | |
| }); | |
| } else { | |
| // Export regular array data | |
| worksheetData = data; | |
| } | |
| const ws = XLSX.utils.aoa_to_sheet(worksheetData); | |
| const wb = XLSX.utils.book_new(); | |
| XLSX.utils.book_append_sheet(wb, ws, "Extracted Data"); | |
| // Auto-width columns | |
| if (worksheetData.length > 0) { | |
| const colWidths = worksheetData[0].map((_, colIndex) => ({ | |
| wch: Math.max(...worksheetData.map(row => (row[colIndex] || '').toString().length)) + 2 | |
| })); | |
| ws['!cols'] = colWidths; | |
| } | |
| XLSX.writeFile(wb, `${filename}.xlsx`); | |
| showToast('ดาวน์โหลด Excel สำเร็จ'); | |
| } | |
| function exportHTML(filename, data) { | |
| if (!data.length) { | |
| showToast('ไม่มีข้อมูลสำหรับ Export', 'error'); | |
| return; | |
| } | |
| try { | |
| let htmlContent = ''; | |
| if (state.extractedDataWithCoords.length) { | |
| // Export coordinate data | |
| htmlContent = ` | |
| <html lang="th"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>${escapeHtml(state.currentFile?.name || 'เอกสาร')}</title> | |
| <style> | |
| body { | |
| font-family: 'Sarabun', sans-serif; | |
| font-size: 11pt; | |
| line-height: 1.6; | |
| color: #000; | |
| background: white; | |
| margin: 0; | |
| padding: 20px; | |
| } | |
| .document-container { | |
| width: 100%; | |
| background: white; | |
| margin: 0 auto; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| .content-section { | |
| margin: 0; | |
| } | |
| .row { | |
| margin-bottom: 12px; | |
| padding: 8px; | |
| border-bottom: 1px solid #eee; | |
| } | |
| .coord-info { | |
| font-size: 9pt; | |
| color: #666; | |
| font-family: monospace; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="document-container"> | |
| <div class="content-section"> | |
| ${data.map(row => | |
| `<div class="row"> | |
| ${row.cells ? row.cells.map(cell => | |
| `<div> | |
| <span>${escapeHtml(cell.text || '')}</span> | |
| <span class="coord-info">[${cell.coordinates.x},${cell.coordinates.y}]</span> | |
| </div>` | |
| ).join('') : ''} | |
| </div>` | |
| ).join('')} | |
| </div> | |
| </div> | |
| </body> | |
| </html>`; | |
| } else { | |
| // Export regular array data | |
| htmlContent = ` | |
| <html lang="th"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>${escapeHtml(state.currentFile?.name || 'เอกสาร')}</title> | |
| <style> | |
| body { | |
| font-family: 'Sarabun', sans-serif; | |
| font-size: 11pt; | |
| line-height: 1.6; | |
| color: #000; | |
| background: white; | |
| margin: 0; | |
| padding: 20px; | |
| } | |
| .document-container { | |
| width: 100%; | |
| background: white; | |
| margin: 0 auto; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| .content-section { | |
| margin: 0; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="document-container"> | |
| <div class="content-section"> | |
| ${data.map(row => | |
| `<div style="margin-bottom: 8px;">${row.map(cell => | |
| escapeHtml(cell || '') | |
| ).join(' ')}</div>` | |
| ).join('')} | |
| </div> | |
| </div> | |
| </body> | |
| </html>`; | |
| } | |
| downloadFile(htmlContent, `${filename}.html`, 'text/html'); | |
| showToast('ดาวน์โหลด HTML สำเร็จ'); | |
| } catch (error) { | |
| console.error('HTML Export Error:', error); | |
| showToast('เกิดข้อผิดพลาดในการ Export', 'error'); | |
| } | |
| } | |
| function downloadFile(content, filename, mimeType) { | |
| const blob = new Blob([content], { type: mimeType + ';charset=utf-8' }); | |
| 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); | |
| } | |
| // Utility Functions | |
| function copyAllData() { | |
| const dataToCopy = state.extractedDataWithCoords.length ? state.extractedDataWithCoords : state.extractedData; | |
| if (!dataToCopy.length) return; | |
| let text = ''; | |
| if (state.extractedDataWithCoords.length) { | |
| text = dataToCopy.map(row => | |
| row.cells ? row.cells.map(cell => cell.text || '').join(' | ') : '' | |
| ).join('\n'); | |
| } else { | |
| text = dataToCopy.map(row => row.join(' | ')).join('\n'); | |
| } | |
| navigator.clipboard.writeText(text).then(() => { | |
| showToast('คัดลอกข้อมูลทั้งหมดแล้ว'); | |
| }).catch(() => { | |
| showToast('ไม่สามารถคัดลอกได้', 'error'); | |
| }); | |
| } | |
| function copyRow(row, index) { | |
| const text = row.join(' | '); | |
| navigator.clipboard.writeText(text).then(() => { | |
| showToast(`คัดลอกแถวที่ ${index + 1} แล้ว`); | |
| }); | |
| } | |
| function toggleJsonPreview() { | |
| if (!elements.jsonPreview || !elements.jsonChevron) return; | |
| const isHidden = elements.jsonPreview.classList.contains('hidden'); | |
| if (isHidden) { | |
| elements.jsonPreview.classList.remove('hidden'); | |
| elements.jsonChevron.style.transform = 'rotate(180deg)'; | |
| } else { | |
| elements.jsonPreview.classList.add('hidden'); | |
| elements.jsonChevron.style.transform = 'rotate(0deg)'; | |
| } | |
| } | |
| function resetApplication() { | |
| // Abort any ongoing operations | |
| if (state.abortController) { | |
| state.abortController.abort(); | |
| } | |
| if (state.ocrWorker) { | |
| state.ocrWorker.terminate(); | |
| state.ocrWorker = null; | |
| } | |
| state.currentFile = null; | |
| state.fileType = null; | |
| state.pdfDoc = null; | |
| state.currentPage = 1; | |
| state.totalPages = 0; | |
| state.extractedData = []; | |
| state.extractedDataWithCoords = []; | |
| state.isProcessing = false; | |
| state.abortController = null; | |
| if (elements.uploadSection) elements.uploadSection.classList.remove('hidden'); | |
| if (elements.previewSection) elements.previewSection.classList.add('hidden'); | |
| if (elements.resultsSection) elements.resultsSection.classList.add('hidden'); | |
| if (elements.previewContainer) elements.previewContainer.innerHTML = ''; | |
| if (elements.pageSelector) { | |
| elements.pageSelector.innerHTML = ''; | |
| elements.pageSelector.classList.add('hidden'); | |
| } | |
| if (elements.jsonContent) elements.jsonContent.textContent = ''; | |
| showToast('รีเซ็ตระบบเรียบร้อย'); | |
| } | |
| function showProcessingModal(title, status, percent) { | |
| if (!elements.processingModal) return; | |
| if (elements.processingTitle) elements.processingTitle.textContent = title; | |
| if (elements.processingStatus) elements.processingStatus.textContent = status; | |
| if (elements.progressBar) elements.progressBar.style.width = percent + '%'; | |
| if (elements.progressPercent) elements.progressPercent.textContent = Math.round(percent) + '%'; | |
| elements.processingModal.classList.remove('hidden'); | |
| } | |
| function updateProcessingProgress(status, percent) { | |
| if (!elements.processingModal) return; | |
| if (elements.processingStatus) elements.processingStatus.textContent = status; | |
| if (elements.progressBar) elements.progressBar.style.width = percent + '%'; | |
| if (elements.progressPercent) elements.progressPercent.textContent = Math.round(percent) + '%'; | |
| } | |
| function hideProcessingModal() { | |
| if (elements.processingModal) { | |
| elements.processingModal.classList.add('hidden'); | |
| } | |
| } | |
| function showToast(message, type = 'success') { | |
| if (!elements.toast || !elements.toastMessage) return; | |
| elements.toastMessage.textContent = message; | |
| const icon = elements.toast.querySelector('i'); | |
| if (type === 'error') { | |
| icon.classList.remove('text-green-400'); | |
| icon.classList.add('text-red-400'); | |
| icon.setAttribute('data-feather', 'alert-circle'); | |
| } else { | |
| icon.classList.remove('text-red-400'); | |
| icon.classList.add('text-green-400'); | |
| icon.setAttribute('data-feather', 'check-circle'); | |
| } | |
| feather.replace(); | |
| elements.toast.classList.remove('opacity-0', 'translate-y-10', 'pointer-events-none'); | |
| setTimeout(() => { | |
| elements.toast.classList.add('opacity-0', 'translate-y-10', 'pointer-events-none'); | |
| }, 3000); | |
| } | |
| function escapeHtml(text) { | |
| const div = document.createElement('div'); | |
| div.textContent = text; | |
| return div.innerHTML; | |
| } | |