Created
September 26, 2025 00:50
-
-
Save lardratboy/c1e8361696040d7fbd5097216e63a183 to your computer and use it in GitHub Desktop.
IFF/RIFF-like data explorer, processes chunks and produces a large amount of png images
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>VOMIT PNG's 0.5.1 - CHUNKY DATA EXPLORER</title> | |
| <style> | |
| body { | |
| font-family: Arial, sans-serif; | |
| line-height: 1.6; | |
| margin: 0; | |
| padding: 20px; | |
| background-color: #f5f5f5; | |
| color: #333; | |
| } | |
| h1, h2, h3 { | |
| margin-top: 0; | |
| } | |
| .container { | |
| max-width: 1200px; | |
| margin: 0 auto; | |
| } | |
| .panel { | |
| background: white; | |
| border-radius: 4px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.12); | |
| padding: 20px; | |
| margin-bottom: 20px; | |
| } | |
| .control-row { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 15px; | |
| margin-bottom: 15px; | |
| } | |
| .control-group { | |
| flex: 1; | |
| min-width: 200px; | |
| } | |
| label { | |
| display: block; | |
| margin-bottom: 5px; | |
| font-weight: bold; | |
| } | |
| input[type="text"], input[type="number"] { | |
| width: 100%; | |
| padding: 8px; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| } | |
| button { | |
| background: #4285f4; | |
| color: white; | |
| border: none; | |
| padding: 10px 20px; | |
| border-radius: 4px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| } | |
| button:hover { | |
| background: #3367d6; | |
| } | |
| button:disabled { | |
| background: #ccc; | |
| cursor: not-allowed; | |
| } | |
| .button-group { | |
| display: flex; | |
| gap: 10px; | |
| } | |
| #cancel-btn { | |
| background: #d93025; | |
| display: none; | |
| } | |
| #cancel-btn:hover { | |
| background: #b52d20; | |
| } | |
| .checkbox-group { | |
| display: flex; | |
| align-items: center; | |
| } | |
| .checkbox-group label { | |
| margin: 0 0 0 5px; | |
| } | |
| #log { | |
| background-color: #f8f8f8; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| padding: 10px; | |
| height: 200px; | |
| overflow-y: auto; | |
| font-family: monospace; | |
| white-space: pre-wrap; | |
| margin-bottom: 20px; | |
| } | |
| #images { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 20px; | |
| } | |
| .image-container { | |
| background: #f9f9f9; | |
| border: 1px solid #ddd; | |
| border-radius: 4px; | |
| padding: 10px; | |
| box-shadow: 0 1px 2px rgba(0,0,0,0.1); | |
| } | |
| .image-container canvas { | |
| max-width: 100%; | |
| display: block; | |
| } | |
| .image-title { | |
| font-weight: bold; | |
| margin-bottom: 5px; | |
| } | |
| .image-info { | |
| font-size: 14px; | |
| margin-top: 5px; | |
| color: #666; | |
| } | |
| .bundle { | |
| width: 100%; | |
| } | |
| .cancelled { | |
| color: #d93025; | |
| font-weight: bold; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="panel"> | |
| <h1>VOMIT PNG's 0.5.1 - CHUNKY DATA EXPLORER</h1> | |
| <p>This tool is able to dump IFF-like chunk files (4 byte ascii id, followed by a big endian 32 bit integer, followed by the contained data. Each chunk header includes the size of data and the size of the header, making this slightly different than standard IFF/RIFF chunks).</p> | |
| </div> | |
| <div class="panel"> | |
| <h2>Settings</h2> | |
| <div class="control-row"> | |
| <div class="control-group"> | |
| <label for="file-input">Select File:</label> | |
| <input type="file" id="file-input"> | |
| </div> | |
| <div class="control-group"> | |
| <label for="output-format">Output Format:</label> | |
| <input type="text" id="output-format" value="%4s_%08d.png"> | |
| </div> | |
| <div class="control-group"> | |
| <label for="xor-key">XOR Key:</label> | |
| <input type="number" id="xor-key" value="105"> | |
| </div> | |
| </div> | |
| <div class="control-row"> | |
| <div class="control-group"> | |
| <label for="page-width">Page Width:</label> | |
| <input type="number" id="page-width" value="1024"> | |
| </div> | |
| <div class="control-group"> | |
| <label for="page-height">Page Height:</label> | |
| <input type="number" id="page-height" value="1024"> | |
| </div> | |
| <div class="control-group"> | |
| <div class="checkbox-group"> | |
| <input type="checkbox" id="bundle"> | |
| <label for="bundle">Bundle</label> | |
| </div> | |
| <div class="checkbox-group"> | |
| <input type="checkbox" id="gray"> | |
| <label for="gray">Grayscale</label> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <div class="button-group"> | |
| <button id="process-btn">Process File</button> | |
| <button id="cancel-btn">Cancel</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <h2>Progress</h2> | |
| <div id="progress-container" style="display: none;"> | |
| <progress id="progress-bar" value="0" max="100" style="width: 100%; height: 20px;"></progress> | |
| <div id="progress-text">Starting...</div> | |
| </div> | |
| <h2>Log</h2> | |
| <pre id="log"></pre> | |
| </div> | |
| <div class="panel"> | |
| <h2>Results</h2> | |
| <div id="stats"></div> | |
| <div id="images"></div> | |
| </div> | |
| </div> | |
| <script> | |
| // Global cancellation flag | |
| let processingCancelled = false; | |
| // Rectangle packing system | |
| class Rect { | |
| constructor(left, top, right, bottom) { | |
| this.left = left; | |
| this.top = top; | |
| this.right = right; | |
| this.bottom = bottom; | |
| } | |
| width() { | |
| return this.right - this.left; | |
| } | |
| height() { | |
| return this.bottom - this.top; | |
| } | |
| } | |
| class Page { | |
| constructor(width, height) { | |
| this.width = width; | |
| this.height = height; | |
| this.free_rects = [new Rect(0, 0, width, height)]; | |
| this.occupied_rects = []; | |
| } | |
| *external_clipped_rects(a, b) { | |
| let top = a.top, bottom = a.bottom; | |
| if (a.top < b.top) { | |
| top = b.top; | |
| yield new Rect(a.left, a.top, a.right, b.top); | |
| } | |
| if (a.bottom > b.bottom) { | |
| bottom = b.bottom; | |
| yield new Rect(a.left, b.bottom, a.right, a.bottom); | |
| } | |
| if (a.left < b.left) { | |
| yield new Rect(a.left, top, b.left, bottom); | |
| } | |
| if (a.right > b.right) { | |
| yield new Rect(b.right, top, a.right, bottom); | |
| } | |
| } | |
| insert(width, height) { | |
| for (let i = 0; i < this.free_rects.length; i++) { | |
| const free_rect = this.free_rects[i]; | |
| if (free_rect.width() < width || free_rect.height() < height) continue; | |
| const rect = new Rect( | |
| free_rect.left, | |
| free_rect.top, | |
| free_rect.left + width, | |
| free_rect.top + height | |
| ); | |
| this.occupied_rects.push(rect); | |
| this.free_rects.splice(i, 1); | |
| const free_count = this.free_rects.length; | |
| for (const clipped_rect of this.external_clipped_rects(free_rect, rect)) { | |
| this.free_rects.push(clipped_rect); | |
| } | |
| if (free_count !== this.free_rects.length) { | |
| this.free_rects.sort((a, b) => a.height() - b.height()); | |
| } | |
| return rect; | |
| } | |
| return null; | |
| } | |
| calculate_efficiency() { | |
| const total_area = this.width * this.height; | |
| const used_area = this.occupied_rects.reduce((sum, rect) => | |
| sum + rect.width() * rect.height(), 0); | |
| return used_area / total_area; | |
| } | |
| } | |
| class Packer { | |
| constructor(width, height) { | |
| this.pages = [new Page(width, height)]; | |
| this.page_width = width; | |
| this.page_height = height; | |
| } | |
| insert(width, height) { | |
| for (const page of this.pages) { | |
| const rect = page.insert(width, height); | |
| if (rect) return [page, rect]; | |
| } | |
| const new_page = new Page(this.page_width, this.page_height); | |
| this.pages.push(new_page); | |
| return [new_page, new_page.insert(width, height)]; | |
| } | |
| } | |
| // Chunk class for data parsing | |
| class Chunk { | |
| constructor(id, offset, depth = 0) { | |
| this.id = id; | |
| this.offset = offset; | |
| this.depth = depth; | |
| this.children = []; | |
| this.data = null; | |
| } | |
| } | |
| // DataChunkCollector equivalent | |
| class DataChunkCollector { | |
| constructor(outputFormatter = null, saveAsGrayscale = false, bundle = false, pageWidth = 1024, pageHeight = 1024) { | |
| this.outputFormatter = outputFormatter; | |
| this.nextNumber = 1; | |
| this.saveAsGrayscale = saveAsGrayscale; | |
| this.savePng = outputFormatter !== null; | |
| this.bundle = bundle; | |
| this.imagesByTypes = {}; | |
| this.bundledImages = []; | |
| this.pageWidth = pageWidth; | |
| this.pageHeight = pageHeight; | |
| } | |
| // Break up image processing into smaller tasks to prevent browser freezing | |
| async processImageData(chunk, canvas, ctx, width, height, isGrayscale) { | |
| if (processingCancelled) return null; | |
| const actualSize = chunk.data.length; | |
| const imgData = ctx.createImageData(width, height); | |
| const data = imgData.data; | |
| // Process in chunks to avoid UI freezing for large images | |
| const pixelsPerBatch = 100000; // Adjust based on performance testing | |
| if (isGrayscale) { | |
| // Grayscale image processing | |
| for (let i = 0; i < actualSize; i += pixelsPerBatch) { | |
| if (processingCancelled) return null; | |
| const end = Math.min(i + pixelsPerBatch, actualSize); | |
| for (let j = i; j < end; j++) { | |
| const val = chunk.data[j]; | |
| const idx = j * 4; | |
| data[idx] = val; // R | |
| data[idx + 1] = val; // G | |
| data[idx + 2] = val; // B | |
| data[idx + 3] = 255; // A | |
| } | |
| if (actualSize > pixelsPerBatch) { | |
| await new Promise(resolve => setTimeout(resolve, 0)); | |
| } | |
| } | |
| // Fill the rest with black pixels (already alpha 255) | |
| for (let i = actualSize; i < width * height; i++) { | |
| const idx = i * 4; | |
| data[idx + 3] = 255; // A for remaining pixels | |
| } | |
| } else { | |
| // RGB image processing | |
| let j = 0; | |
| for (let i = 0; i < actualSize; i += 3) { | |
| if (processingCancelled) return null; | |
| if (j % pixelsPerBatch === 0 && j > 0) { | |
| await new Promise(resolve => setTimeout(resolve, 0)); | |
| } | |
| const r = i < actualSize ? chunk.data[i] : 0; | |
| const g = (i + 1) < actualSize ? chunk.data[i + 1] : 0; | |
| const b = (i + 2) < actualSize ? chunk.data[i + 2] : 0; | |
| const idx = j * 4; | |
| data[idx] = r; // R | |
| data[idx + 1] = g; // G | |
| data[idx + 2] = b; // B | |
| data[idx + 3] = 255; // A | |
| j++; | |
| } | |
| // Fill the rest with black pixels (with alpha 255) | |
| for (let i = j; i < width * height; i++) { | |
| const idx = i * 4; | |
| data[idx + 3] = 255; // A | |
| } | |
| } | |
| if (processingCancelled) return null; | |
| // Put the image data on the canvas | |
| ctx.putImageData(imgData, 0, 0); | |
| return canvas; | |
| } | |
| async collect(chunk) { | |
| if (processingCancelled) return; | |
| // Filter chunks by ID | |
| if (!['AWIZ', 'AKOS', 'SDAT', 'SOUN', 'DIGI', 'RMIM', 'COST', 'FORM'].includes(chunk.id)) return; | |
| const actualSize = chunk.data.length; | |
| if (this.savePng) { | |
| const filename = this.outputFormatter | |
| .replace('%4s', chunk.id) | |
| .replace('%08d', String(this.nextNumber).padStart(8, '0')); | |
| this.nextNumber++; | |
| let side, width, height; | |
| let canvas, ctx; | |
| if (this.saveAsGrayscale) { | |
| // Follow Python's logic for grayscale | |
| const alignedSize = actualSize; | |
| side = Math.floor(Math.max(Math.ceil(Math.sqrt(alignedSize)), 1)); | |
| width = side; | |
| height = side; | |
| canvas = document.createElement('canvas'); | |
| canvas.width = width; | |
| canvas.height = height; | |
| ctx = canvas.getContext('2d'); | |
| } else { | |
| // Follow Python's logic for RGB | |
| const alignedSize = Math.ceil((actualSize + 2) / 3); | |
| side = Math.floor(Math.max(Math.ceil(Math.sqrt(alignedSize)), 1)); | |
| width = side; | |
| height = side; | |
| canvas = document.createElement('canvas'); | |
| canvas.width = width; | |
| canvas.height = height; | |
| ctx = canvas.getContext('2d'); | |
| } | |
| // Process image data asynchronously for large chunks | |
| const result = await this.processImageData(chunk, canvas, ctx, width, height, this.saveAsGrayscale); | |
| if (!result) return; // Cancelled during processing | |
| // Store image info | |
| if (this.bundle) { | |
| if (!this.imagesByTypes[chunk.id]) { | |
| this.imagesByTypes[chunk.id] = []; | |
| } | |
| // Save image and its data | |
| const img = { | |
| canvas: canvas, | |
| chunk: chunk, | |
| width: width, | |
| height: height, | |
| filename: filename | |
| }; | |
| this.imagesByTypes[chunk.id].push(img); | |
| } else { | |
| // For non-bundled, display directly | |
| this.displayImage(canvas, filename, chunk.id); | |
| } | |
| } | |
| } | |
| displayImage(canvas, filename, chunkId) { | |
| const container = document.createElement('div'); | |
| container.className = 'image-container'; | |
| const title = document.createElement('div'); | |
| title.className = 'image-title'; | |
| title.textContent = filename; | |
| container.appendChild(title); | |
| container.appendChild(canvas); | |
| const info = document.createElement('div'); | |
| info.className = 'image-info'; | |
| info.textContent = `Chunk ID: ${chunkId}, Size: ${canvas.width}x${canvas.height}`; | |
| container.appendChild(info); | |
| document.getElementById('images').appendChild(container); | |
| } | |
| async saveBundles() { | |
| if (!this.bundle || processingCancelled) return; | |
| log("Saving bundles..."); | |
| const packers = []; | |
| // Pack images by type | |
| for (const id in this.imagesByTypes) { | |
| if (processingCancelled) return; | |
| log(`Processing ${id} images...`); | |
| // Sort by height, largest first, as in Python | |
| this.imagesByTypes[id].sort((a, b) => b.height - a.height); | |
| const packer = new Packer(this.pageWidth, this.pageHeight); | |
| packers.push(packer); | |
| packer.rectsByImage = {}; | |
| packer.id = id; | |
| // Process in batches to avoid UI freezing | |
| const images = this.imagesByTypes[id]; | |
| const batchSize = 100; | |
| for (let i = 0; i < images.length; i += batchSize) { | |
| if (processingCancelled) return; | |
| const end = Math.min(i + batchSize, images.length); | |
| for (let j = i; j < end; j++) { | |
| const img = images[j]; | |
| const [page, rect] = packer.insert(img.width, img.height); | |
| if (!rect) { | |
| console.error(`Failed to insert ${img.width}x${img.height}`); | |
| log(`ERROR:: failed to insert width=${img.width}, height=${img.height} ::ERROR`); | |
| continue; | |
| } | |
| rect.img = img; | |
| } | |
| // Yield to browser after each batch | |
| updateProgress(end, images.length, `Packing ${id} images`); | |
| await new Promise(resolve => setTimeout(resolve, 0)); | |
| } | |
| } | |
| if (processingCancelled) return; | |
| log("Creating bundle images..."); | |
| // Build and save bundles for each packer | |
| for (let packerIndex = 0; packerIndex < packers.length; packerIndex++) { | |
| if (processingCancelled) return; | |
| const packer = packers[packerIndex]; | |
| for (let i = 0; i < packer.pages.length; i++) { | |
| if (processingCancelled) return; | |
| const page = packer.pages[i]; | |
| updateProgress(i + 1, packer.pages.length, `Creating ${packer.id} bundle images`); | |
| // Create canvas for the page | |
| const canvas = document.createElement('canvas'); | |
| canvas.width = packer.page_width; | |
| canvas.height = packer.page_height; | |
| const ctx = canvas.getContext('2d'); | |
| ctx.fillStyle = 'black'; | |
| ctx.fillRect(0, 0, canvas.width, canvas.height); | |
| // Draw images in batches | |
| const rects = page.occupied_rects; | |
| const rectBatchSize = 100; | |
| for (let j = 0; j < rects.length; j += rectBatchSize) { | |
| if (processingCancelled) return; | |
| const endRect = Math.min(j + rectBatchSize, rects.length); | |
| for (let k = j; k < endRect; k++) { | |
| const rect = rects[k]; | |
| ctx.drawImage(rect.img.canvas, rect.left, rect.top); | |
| } | |
| // Yield to browser after each batch | |
| if (rects.length > rectBatchSize) { | |
| await new Promise(resolve => setTimeout(resolve, 0)); | |
| } | |
| } | |
| // Generate filename | |
| const filename = this.outputFormatter | |
| .replace('%4s', packer.id) | |
| .replace('%08d', String(i + 1).padStart(8, '0')); | |
| // Save canvas info | |
| canvas.filename = filename; | |
| canvas.page = page; | |
| this.bundledImages.push(canvas); | |
| // Display the bundle | |
| this.displayBundle(canvas, filename, packer.id, page); | |
| // Yield to browser after each page | |
| await new Promise(resolve => setTimeout(resolve, 0)); | |
| } | |
| } | |
| if (!processingCancelled) { | |
| // Display stats | |
| this.displayStats(); | |
| } | |
| } | |
| displayBundle(canvas, filename, id, page) { | |
| const container = document.createElement('div'); | |
| container.className = 'image-container bundle'; | |
| const title = document.createElement('div'); | |
| title.className = 'image-title'; | |
| title.textContent = filename; | |
| container.appendChild(title); | |
| container.appendChild(canvas); | |
| const info = document.createElement('div'); | |
| info.className = 'image-info'; | |
| const efficiency = page.calculate_efficiency(); | |
| info.textContent = `Chunk ID: ${id}, Efficiency: ${(efficiency * 100).toFixed(2)}%`; | |
| container.appendChild(info); | |
| document.getElementById('images').appendChild(container); | |
| } | |
| displayStats() { | |
| const stats = document.getElementById('stats'); | |
| stats.innerHTML = `<h3>Results</h3>`; | |
| stats.innerHTML += `<p>Total number of bundles: ${this.bundledImages.length}</p>`; | |
| let statsList = '<ul>'; | |
| for (let i = 0; i < this.bundledImages.length; i++) { | |
| const canvas = this.bundledImages[i]; | |
| const efficiency = canvas.page.calculate_efficiency(); | |
| statsList += `<li>page ${canvas.filename} efficiency=${efficiency.toFixed(6)}</li>`; | |
| } | |
| statsList += '</ul>'; | |
| stats.innerHTML += statsList; | |
| } | |
| } | |
| // Function to skip possible garbage data | |
| function skipPossibleGarbageData(data, chunk, offset, remainingData) { | |
| if (chunk.id !== 'DIGI') return 0; | |
| const startOffset = offset; | |
| let skipped = 0; | |
| while (skipped < remainingData) { | |
| try { | |
| // Try to read 4 bytes as ASCII | |
| if (offset + skipped + 4 <= data.length) { | |
| // Check if we can form a valid ID | |
| let validAscii = true; | |
| for (let i = 0; i < 4; i++) { | |
| const charCode = data[offset + skipped + i]; | |
| if (charCode < 32 || charCode > 126) { | |
| validAscii = false; | |
| break; | |
| } | |
| } | |
| if (validAscii) { | |
| return 0; | |
| } | |
| } | |
| } catch (e) { | |
| console.log('Error trying to skip garbage:', e); | |
| } | |
| skipped++; | |
| } | |
| return skipped; | |
| } | |
| // Function to process chunks asynchronously | |
| async function processChunk(data, parentChunk, offset, remainingData, depth = 0, collector = null, bundle = false, progressCallback = null) { | |
| if (processingCancelled) return offset; | |
| const validParentIDs = ['MULT', 'WRAP', 'TALK', 'TLKB', 'LECF', 'LFLF', 'SONG', 'NEST', 'RMDA', 'OBIM', 'ROOM']; | |
| if (parentChunk.id !== null && !validParentIDs.includes(parentChunk.id)) { | |
| log(' '.repeat(depth) + `${parentChunk.id} size=${remainingData}`); | |
| // Extract the chunk data | |
| const startPos = offset - 8; | |
| const endPos = offset + remainingData; | |
| parentChunk.data = data.slice(startPos, endPos); | |
| if (collector && !processingCancelled) await collector.collect(parentChunk); | |
| return offset + remainingData; | |
| } | |
| log(' '.repeat(depth) + `processing ${parentChunk.id || 'root'}`); | |
| let currentOffset = offset; | |
| let chunksProcessed = 0; | |
| while (remainingData > 8 && !processingCancelled) { | |
| // Yield to the browser every 50 chunks to prevent UI freezing | |
| if (chunksProcessed % 50 === 0) { | |
| await new Promise(resolve => setTimeout(resolve, 0)); | |
| if (progressCallback) { | |
| progressCallback(currentOffset, data.length); | |
| } | |
| } | |
| // Read chunk ID (4 bytes) | |
| const id = String.fromCharCode( | |
| data[currentOffset], | |
| data[currentOffset + 1], | |
| data[currentOffset + 2], | |
| data[currentOffset + 3] | |
| ); | |
| // Read chunk size (4 bytes, big endian) | |
| const size = (data[currentOffset + 4] << 24) | | |
| (data[currentOffset + 5] << 16) | | |
| (data[currentOffset + 6] << 8) | | |
| data[currentOffset + 7]; | |
| const chunk = new Chunk(id, currentOffset, depth); | |
| parentChunk.children.push(chunk); | |
| currentOffset += 8; | |
| // Process the chunk asynchronously | |
| const newOffset = await processChunk(data, chunk, currentOffset, size - 8, depth + 1, collector, bundle, progressCallback); | |
| // If newOffset was returned, use it, otherwise calculate | |
| currentOffset = newOffset || (currentOffset + size - 8); | |
| remainingData -= size; | |
| // Skip possible garbage data | |
| const skipped = skipPossibleGarbageData(data, chunk, currentOffset, remainingData); | |
| remainingData -= skipped; | |
| currentOffset += skipped; | |
| chunksProcessed++; | |
| } | |
| return currentOffset; | |
| } | |
| // Main function to parse chunks from a file asynchronously | |
| async function parseChunksForFile(data, xorKey = null, collector = null, bundle = false, progressCallback = null) { | |
| if (processingCancelled) return null; | |
| // Apply XOR if needed (this will also be done asynchronously for large files) | |
| let processedData = new Uint8Array(data); | |
| if (xorKey !== null && xorKey !== 0) { | |
| log('Applying XOR decryption...'); | |
| const xorValue = xorKey & 0xFF; | |
| processedData = new Uint8Array(data.length); | |
| // Process in chunks of 1MB to avoid UI freezing | |
| const chunkSize = 1024 * 1024; | |
| for (let i = 0; i < data.length; i += chunkSize) { | |
| if (processingCancelled) return null; | |
| const end = Math.min(i + chunkSize, data.length); | |
| for (let j = i; j < end; j++) { | |
| processedData[j] = data[j] ^ xorValue; | |
| } | |
| // Yield to the browser after each chunk | |
| if (progressCallback) { | |
| progressCallback(end, data.length, 'Decrypting'); | |
| } | |
| await new Promise(resolve => setTimeout(resolve, 0)); | |
| } | |
| } | |
| if (processingCancelled) return null; | |
| log('Processing chunks...'); | |
| const rootChunk = new Chunk(null, 0); | |
| await processChunk(processedData, rootChunk, 0, processedData.length, 0, collector, bundle, progressCallback); | |
| return rootChunk; | |
| } | |
| // Helper function for logging | |
| function log(message) { | |
| const logElement = document.getElementById('log'); | |
| logElement.innerHTML += message + '\n'; | |
| logElement.scrollTop = logElement.scrollHeight; | |
| console.log(message); | |
| } | |
| // Progress tracking | |
| function updateProgress(current, total, stage = 'Processing') { | |
| const percent = Math.floor((current / total) * 100); | |
| const progressElement = document.getElementById('progress-bar'); | |
| const progressTextElement = document.getElementById('progress-text'); | |
| if (progressElement && progressTextElement) { | |
| progressElement.value = percent; | |
| progressTextElement.textContent = `${stage}: ${percent}% (${current}/${total})`; | |
| } | |
| } | |
| // Cancel function | |
| function cancelProcessing() { | |
| processingCancelled = true; | |
| log('<span class="cancelled">Processing cancelled by user</span>'); | |
| // Reset UI state | |
| const processBtn = document.getElementById('process-btn'); | |
| const cancelBtn = document.getElementById('cancel-btn'); | |
| processBtn.disabled = false; | |
| processBtn.textContent = 'Process File'; | |
| cancelBtn.style.display = 'none'; | |
| updateProgress(0, 100, 'Cancelled'); | |
| } | |
| // Main process function (async) | |
| async function processFile() { | |
| // Reset cancellation flag | |
| processingCancelled = false; | |
| // Clear previous output | |
| document.getElementById('log').innerHTML = ''; | |
| document.getElementById('stats').innerHTML = ''; | |
| document.getElementById('images').innerHTML = ''; | |
| // Get form values | |
| const fileInput = document.getElementById('file-input'); | |
| const outputFormat = document.getElementById('output-format').value; | |
| const xorKey = parseInt(document.getElementById('xor-key').value, 10); | |
| const pageWidth = parseInt(document.getElementById('page-width').value, 10); | |
| const pageHeight = parseInt(document.getElementById('page-height').value, 10); | |
| const bundle = document.getElementById('bundle').checked; | |
| const gray = document.getElementById('gray').checked; | |
| if (!fileInput.files.length) { | |
| log('Error: No file selected'); | |
| return; | |
| } | |
| const file = fileInput.files[0]; | |
| log(`Processing file: ${file.name} (${(file.size / (1024 * 1024)).toFixed(2)} MB)`); | |
| // Update UI state | |
| const processBtn = document.getElementById('process-btn'); | |
| const cancelBtn = document.getElementById('cancel-btn'); | |
| processBtn.disabled = true; | |
| processBtn.textContent = 'Processing...'; | |
| cancelBtn.style.display = 'inline-block'; | |
| document.getElementById('progress-container').style.display = 'block'; | |
| try { | |
| // Read the file | |
| const arrayBuffer = await new Promise((resolve, reject) => { | |
| const reader = new FileReader(); | |
| reader.onload = e => resolve(e.target.result); | |
| reader.onerror = e => reject(e); | |
| reader.readAsArrayBuffer(file); | |
| }); | |
| if (processingCancelled) return; | |
| const data = new Uint8Array(arrayBuffer); | |
| log(`File loaded, size: ${(data.length / (1024 * 1024)).toFixed(2)} MB`); | |
| // Create collector | |
| const collector = new DataChunkCollector(outputFormat, gray, bundle, pageWidth, pageHeight); | |
| // Parse chunks asynchronously | |
| log('Parsing chunks...'); | |
| const fileChunks = await parseChunksForFile(data, xorKey, collector, bundle, updateProgress); | |
| if (processingCancelled) return; | |
| // Save bundles if needed | |
| if (bundle) { | |
| await collector.saveBundles(); | |
| } | |
| if (!processingCancelled) { | |
| log('Processing complete!'); | |
| updateProgress(100, 100, 'Complete'); | |
| } | |
| } catch (error) { | |
| if (!processingCancelled) { | |
| log(`Error: ${error.message}`); | |
| console.error(error); | |
| } | |
| } finally { | |
| // Reset UI state | |
| processBtn.disabled = false; | |
| processBtn.textContent = 'Process File'; | |
| cancelBtn.style.display = 'none'; | |
| } | |
| } | |
| // Add event listeners | |
| document.getElementById('process-btn').addEventListener('click', processFile); | |
| document.getElementById('cancel-btn').addEventListener('click', cancelProcessing); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment