Advertisement
Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- // Keal's "MVX: Minimal Video Exchange" GW-BASIC/QBASIC TIL/BSV Compression Tool
- // Shared :: "Legacy/Graphics/Load TIL.js"
- // Function to load a TIL file with variable bit depth, including support for non-standard bit depths and padding.
- function loadTIL(fileData, tileWidth, tileHeight, bitDepth, padPerImage = true) {
- const tileSize = Math.ceil((tileWidth * tileHeight * bitDepth) / 8);
- const tiles = [];
- const numTiles = fileData.length / tileSize;
- let bitPos = 0;
- for (let i = 0; i < numTiles; i++) {
- const tile = new Uint8Array(tileWidth * tileHeight);
- for (let j = 0; j < tileWidth * tileHeight; j++) {
- const byteIndex = Math.floor(bitPos / 8);
- const bitOffset = bitPos % 8;
- switch (bitDepth) {
- case 8:
- tile[j] = fileData[byteIndex]; // Direct byte-per-pixel mapping.
- break;
- case 4:
- tile[j] = (fileData[Math.floor(j / 2)] >> ((j % 2) ? 0 : 4)) & 0x0F;
- break;
- case 2:
- tile[j] = (fileData[Math.floor(j / 4)] >> ((3 - (j % 4)) * 2)) & 0x03;
- break;
- case 1:
- tile[j] = (fileData[Math.floor(j / 8)] >> (7 - (j % 8))) & 0x01;
- break;
- case 3:
- tile[j] = (fileData[byteIndex] >> (5 - bitOffset)) & 0x07;
- break;
- case 5:
- tile[j] = (fileData[byteIndex] >> (3 - bitOffset)) & 0x1F;
- break;
- case 6:
- tile[j] = (fileData[byteIndex] >> (2 - bitOffset)) & 0x3F;
- break;
- case 7:
- tile[j] = (fileData[byteIndex] >> (1 - bitOffset)) & 0x7F;
- break;
- default:
- throw new Error(`Unsupported bit depth: ${bitDepth}`);
- }
- bitPos += bitDepth;
- }
- tiles.push(tile);
- // Align to the next byte boundary if padding is applied per image
- if (padPerImage && bitPos % 8 !== 0) {
- bitPos += 8 - (bitPos % 8);
- }
- }
- return tiles;
- }
- // Shared :: "Legacy/Graphics/Save TIL.js"
- // Function to save a TIL file with variable bit depth, including support for non-standard bit depths and padding.
- function saveTIL(tiles, tileWidth, tileHeight, bitDepth, padPerImage = true) {
- const tileSize = Math.ceil((tileWidth * tileHeight * bitDepth) / 8);
- const numTiles = tiles.length;
- const fileData = new Uint8Array(numTiles * tileSize);
- let bitPos = 0;
- for (let i = 0; i < numTiles; i++) {
- const tile = tiles[i];
- for (let j = 0; j < tileWidth * tileHeight; j++) {
- const byteIndex = Math.floor(bitPos / 8);
- const bitOffset = bitPos % 8;
- switch (bitDepth) {
- case 8:
- fileData[byteIndex] = tile[j]; // Direct byte-per-pixel mapping.
- break;
- case 4:
- fileData[Math.floor(j / 2)] |= (tile[j] & 0x0F) << ((j % 2) ? 0 : 4);
- break;
- case 2:
- fileData[Math.floor(j / 4)] |= (tile[j] & 0x03) << ((3 - (j % 4)) * 2);
- break;
- case 1:
- fileData[Math.floor(j / 8)] |= (tile[j] & 0x01) << (7 - (j % 8));
- break;
- case 3:
- fileData[byteIndex] |= (tile[j] & 0x07) << (5 - bitOffset);
- break;
- case 5:
- fileData[byteIndex] |= (tile[j] & 0x1F) << (3 - bitOffset);
- break;
- case 6:
- fileData[byteIndex] |= (tile[j] & 0x3F) << (2 - bitOffset);
- break;
- case 7:
- fileData[byteIndex] |= (tile[j] & 0x7F) << (1 - bitOffset);
- break;
- default:
- throw new Error(`Unsupported bit depth: ${bitDepth}`);
- }
- bitPos += bitDepth;
- }
- // Align to the next byte boundary if padding is applied per image
- if (padPerImage && bitPos % 8 !== 0) {
- bitPos += 8 - (bitPos % 8);
- }
- }
- return fileData;
- }
- // Shared :: "Legacy/Graphics/Load BSV.js"
- // Function to load a BSV file with variable bit depth (QBASIC BSAVE format) and read dimensions from the header.
- function loadBSV(fileData, bitDepth) {
- const headerSize = 7; // Standard BSAVE header size in bytes.
- // Read the width and height from the header (bytes 3–6)
- const width = fileData[3] | (fileData[4] << 8);
- const height = fileData[5] | (fileData[6] << 8);
- const imageSize = calculateImageSize(bitDepth, width, height);
- const pixelData = fileData.slice(headerSize, headerSize + imageSize);
- const pixels = new Uint8Array(width * height);
- let bitPos = 0;
- for (let j = 0; j < width * height; j++) {
- const byteIndex = Math.floor(bitPos / 8);
- const bitOffset = bitPos % 8;
- switch (bitDepth) {
- case 8:
- pixels[j] = pixelData[j]; // Direct mapping for 8-bit depth.
- break;
- case 4:
- pixels[j] = (pixelData[Math.floor(j / 2)] >> ((j % 2) ? 0 : 4)) & 0x0F;
- break;
- case 2:
- pixels[j] = (pixelData[Math.floor(j / 4)] >> ((3 - (j % 4)) * 2)) & 0x03;
- break;
- case 1:
- pixels[j] = (pixelData[Math.floor(j / 8)] >> (7 - (j % 8))) & 0x01;
- break;
- case 3:
- pixels[j] = (pixelData[byteIndex] >> (5 - bitOffset)) & 0x07;
- break;
- case 5:
- pixels[j] = (pixelData[byteIndex] >> (3 - bitOffset)) & 0x1F;
- break;
- case 6:
- pixels[j] = (pixelData[byteIndex] >> (2 - bitOffset)) & 0x3F;
- break;
- case 7:
- pixels[j] = (pixelData[byteIndex] >> (1 - bitOffset)) & 0x7F;
- break;
- default:
- throw new Error(`Unsupported bit depth: ${bitDepth}`);
- }
- bitPos += bitDepth;
- }
- return { pixels, width, height }; // Return the pixels along with the image dimensions
- }
- // Shared :: "Legacy/Graphics/Save BSV.js"
- // Function to save a BSV file with variable bit depth (QBASIC BSAVE format), writing dimensions into the header.
- function saveBSV(pixels, bitDepth, width, height) {
- const headerSize = 7; // Standard BSAVE header size.
- const imageSize = calculateImageSize(bitDepth, width, height);
- const fileData = new Uint8Array(headerSize + imageSize);
- // Set up the BSAVE header.
- fileData[0] = 0xFD; // 'BSAVE' magic byte.
- fileData[1] = 0x00; // Offset (2 bytes), usually 0x0000 for mode 13h.
- fileData[2] = 0x00;
- // Encode width and height in little-endian format
- fileData[3] = width & 0xFF;
- fileData[4] = (width >> 8) & 0xFF;
- fileData[5] = height & 0xFF;
- fileData[6] = (height >> 8) & 0xFF;
- let bitPos = 0;
- for (let j = 0; j < width * height; j++) {
- const byteIndex = Math.floor(bitPos / 8);
- const bitOffset = bitPos % 8;
- switch (bitDepth) {
- case 8:
- fileData[headerSize + j] = pixels[j]; // Direct byte-per-pixel mapping.
- break;
- case 4:
- fileData[Math.floor(j / 2)] |= (pixels[j] & 0x0F) << ((j % 2) ? 0 : 4);
- break;
- case 2:
- fileData[Math.floor(j / 4)] |= (pixels[j] & 0x03) << ((3 - (j % 4)) * 2);
- break;
- case 1:
- fileData[Math.floor(j / 8)] |= (pixels[j] & 0x01) << (7 - (j % 8));
- break;
- case 3:
- fileData[byteIndex] |= (pixels[j] & 0x07) << (5 - bitOffset);
- break;
- case 5:
- fileData[byteIndex] |= (pixels[j] & 0x1F) << (3 - bitOffset);
- break;
- case 6:
- fileData[byteIndex] |= (pixels[j] & 0x3F) << (2 - bitOffset);
- break;
- case 7:
- fileData[byteIndex] |= (pixels[j] & 0x7F) << (1 - bitOffset);
- break;
- default:
- throw new Error(`Unsupported bit depth: ${bitDepth}`);
- }
- bitPos += bitDepth;
- }
- return fileData;
- }
- // Shared :: "Legacy/Graphics/Support.js"
- // Function to calculate image size based on bit depth.
- function calculateImageSize(bitDepth, width, height) {
- return Math.ceil((width * height * bitDepth) / 8);
- }
- // Example usage
- let tilFile = loadTIL(fileData, 16, 16, 5, true); // Padding between images
- let bsvFile = loadBSV(bsvData, 6);
- let savedTIL = saveTIL(tilFile, 16, 16, 5, false); // Padding after all images
- let savedBSV = saveBSV(bsvFile.pixels, 6, bsvFile.width, bsvFile.height);
- // Shared :: "Legacy/Graphics/HuffmanCompression.js"
- // Function to create a frequency table from the data
- function createFrequencyTable(data) {
- let freqTable = {};
- for (let i = 0; i < data.length; i++) {
- let char = data[i];
- freqTable[char] = (freqTable[char] || 0) + 1;
- }
- return freqTable;
- }
- // Function to create Huffman tree from the frequency table
- function createHuffmanTree(freqTable) {
- let nodes = Object.entries(freqTable).map(([char, freq]) => ({ char, freq }));
- while (nodes.length > 1) {
- nodes.sort((a, b) => a.freq - b.freq);
- let left = nodes.shift();
- let right = nodes.shift();
- let newNode = { char: null, freq: left.freq + right.freq, left, right };
- nodes.push(newNode);
- }
- return nodes[0];
- }
- // Function to generate Huffman codes from the tree
- function generateHuffmanCodes(tree, prefix = '', codes = {}) {
- if (tree.char !== null) {
- codes[tree.char] = prefix;
- } else {
- generateHuffmanCodes(tree.left, prefix + '0', codes);
- generateHuffmanCodes(tree.right, prefix + '1', codes);
- }
- return codes;
- }
- // Function to compress data using Huffman codes
- function huffmanCompress(data, huffmanCodes) {
- let binaryString = '';
- for (let i = 0; i < data.length; i++) {
- binaryString += huffmanCodes[data[i]];
- }
- let byteArray = [];
- for (let i = 0; i < binaryString.length; i += 8) {
- let byte = binaryString.slice(i, i + 8);
- byteArray.push(parseInt(byte.padEnd(8, '0'), 2)); // Pack into bytes
- }
- return new Uint8Array(byteArray);
- }
- // Function to unpack the compressed byte array into a binary string
- function unpackBits(byteArray) {
- let binaryString = '';
- for (let i = 0; i < byteArray.length; i++) {
- let byte = byteArray[i].toString(2).padStart(8, '0');
- binaryString += byte;
- }
- return binaryString;
- }
- // Function to rebuild the Huffman tree from the Huffman codes
- function rebuildHuffmanTree(huffmanCodes) {
- let root = {};
- for (let char in huffmanCodes) {
- let code = huffmanCodes[char];
- let node = root;
- for (let bit of code) {
- if (!node[bit]) node[bit] = {};
- node = node[bit];
- }
- node.char = char;
- }
- return root;
- }
- // Function to decode the compressed binary data using the Huffman tree
- function huffmanDecompress(binaryString, huffmanTree) {
- let originalData = '';
- let node = huffmanTree;
- for (let bit of binaryString) {
- node = node[bit]; // Traverse the tree
- if (node.char) {
- originalData += node.char;
- node = huffmanTree; // Reset to root
- }
- }
- return originalData;
- }
- // Function to compress palette and field data using Huffman compression
- function compressMasterData(palette, field) {
- // Concatenate the palette and field data as a string
- let combinedData = palette + "fieldStart" + field;
- // Create the frequency table
- let freqTable = createFrequencyTable(combinedData);
- // Build the Huffman tree
- let huffmanTree = createHuffmanTree(freqTable);
- // Generate the Huffman codes
- let huffmanCodes = generateHuffmanCodes(huffmanTree);
- // Compress the data
- let compressedData = huffmanCompress(combinedData, huffmanCodes);
- return { compressedData, huffmanCodes };
- }
- // Function to decompress the master data back into palette and field
- function decompressMasterData(compressedData, huffmanCodes, width, height, depth = 32) {
- // Unpack the compressed byte array into a binary string
- let binaryString = unpackBits(compressedData);
- // Rebuild the Huffman tree from the codes
- let huffmanTree = rebuildHuffmanTree(huffmanCodes);
- // Decompress the binary string back into original data
- let originalData = huffmanDecompress(binaryString, huffmanTree);
- // Extract the palette and field from the decompressed data
- let paletteLength = originalData.indexOf("fieldStart");
- let palette = originalData.slice(0, paletteLength);
- let field = originalData.slice(paletteLength + "fieldStart".length);
- // Rebuild the images from the decompressed palette and field data
- let originalTiles = rebuildImages(palette, field, width, height, depth);
- return originalTiles;
- }
- // Function to rebuild images from the original data
- function rebuildImages(palette, field, width, height, depth = 32) {
- let tiles = [];
- let paletteEntries = [];
- // Split the palette into entries
- for (let i = 0; i < palette.length; i += (depth / 4)) {
- paletteEntries.push(palette.slice(i, i + (depth / 4)));
- }
- // Reconstruct the tiles
- for (let i = 0; i < field.length / (width * height); i++) {
- let tile = new Uint8Array(width * height);
- for (let j = 0; j < width * height; j++) {
- // Get the index of the color in the palette
- let paletteIndex = parseInt(field.slice((i * width * height + j) * 2, (i * width * height + j) * 2 + 2), 16);
- let color = parseInt(paletteEntries[paletteIndex], 16);
- tile[j] = color;
- }
- tiles.push(tile);
- }
- return tiles;
- }
- // Function to combine and compress multiple BSV or TIL files
- function combineAndCompressFiles(files) {
- let combinedData = {
- palette: "",
- field: "",
- metadata: []
- };
- files.forEach(file => {
- let { pixels, width, height, bitDepth, type } = file;
- let { palette, field } = asUnique(asHex(pixels, width, height), width, height);
- combinedData.palette += palette;
- combinedData.field += field;
- combinedData.metadata.push({ width, height, bitDepth, type });
- });
- // Compress the combined palette and field data
- return compressMasterData(combinedData.palette, combinedData.field);
- }
- // Function to decompress and reconstruct multiple files
- function decompressAndReconstructFiles(compressedData, huffmanCodes, combinedData) {
- let binaryString = unpackBits(compressedData);
- let huffmanTree = rebuildHuffmanTree(huffmanCodes);
- let originalData = huffmanDecompress(binaryString, huffmanTree);
- let reconstructedFiles = [];
- combinedData.metadata.forEach(metadata => {
- let { width, height, bitDepth, type } = metadata;
- let fieldLength = width * height * 2;
- let field = combinedData.field.slice(0, fieldLength);
- combinedData.field = combinedData.field.slice(fieldLength);
- let paletteLength = field.match(/.{1,2}/g).length * (bitDepth / 4);
- let palette = combinedData.palette.slice(0, paletteLength);
- combinedData.palette = combinedData.palette.slice(paletteLength);
- let pixels = rebuildImages(palette, field, width, height, bitDepth);
- reconstructedFiles.push({ pixels, width, height, bitDepth, type });
- });
- return reconstructedFiles;
- }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement