Advertisement
jargon

Keal's "MVX: Minimal Video Exchange" GW-BASIC/QBASIC TIL/BSV Compression Tool

Sep 26th, 2024
207
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
JavaScript 13.78 KB | Gaming | 0 0
  1. // Keal's "MVX: Minimal Video Exchange" GW-BASIC/QBASIC TIL/BSV Compression Tool
  2.  
  3. // Shared :: "Legacy/Graphics/Load TIL.js"
  4.  
  5. // Function to load a TIL file with variable bit depth, including support for non-standard bit depths and padding.
  6. function loadTIL(fileData, tileWidth, tileHeight, bitDepth, padPerImage = true) {
  7.     const tileSize = Math.ceil((tileWidth * tileHeight * bitDepth) / 8);
  8.     const tiles = [];
  9.     const numTiles = fileData.length / tileSize;
  10.  
  11.     let bitPos = 0;
  12.  
  13.     for (let i = 0; i < numTiles; i++) {
  14.         const tile = new Uint8Array(tileWidth * tileHeight);
  15.  
  16.         for (let j = 0; j < tileWidth * tileHeight; j++) {
  17.             const byteIndex = Math.floor(bitPos / 8);
  18.             const bitOffset = bitPos % 8;
  19.  
  20.             switch (bitDepth) {
  21.                 case 8:
  22.                     tile[j] = fileData[byteIndex]; // Direct byte-per-pixel mapping.
  23.                     break;
  24.                 case 4:
  25.                     tile[j] = (fileData[Math.floor(j / 2)] >> ((j % 2) ? 0 : 4)) & 0x0F;
  26.                     break;
  27.                 case 2:
  28.                     tile[j] = (fileData[Math.floor(j / 4)] >> ((3 - (j % 4)) * 2)) & 0x03;
  29.                     break;
  30.                 case 1:
  31.                     tile[j] = (fileData[Math.floor(j / 8)] >> (7 - (j % 8))) & 0x01;
  32.                     break;
  33.                 case 3:
  34.                     tile[j] = (fileData[byteIndex] >> (5 - bitOffset)) & 0x07;
  35.                     break;
  36.                 case 5:
  37.                     tile[j] = (fileData[byteIndex] >> (3 - bitOffset)) & 0x1F;
  38.                     break;
  39.                 case 6:
  40.                     tile[j] = (fileData[byteIndex] >> (2 - bitOffset)) & 0x3F;
  41.                     break;
  42.                 case 7:
  43.                     tile[j] = (fileData[byteIndex] >> (1 - bitOffset)) & 0x7F;
  44.                     break;
  45.                 default:
  46.                     throw new Error(`Unsupported bit depth: ${bitDepth}`);
  47.             }
  48.  
  49.             bitPos += bitDepth;
  50.         }
  51.  
  52.         tiles.push(tile);
  53.  
  54.         // Align to the next byte boundary if padding is applied per image
  55.         if (padPerImage && bitPos % 8 !== 0) {
  56.             bitPos += 8 - (bitPos % 8);
  57.         }
  58.     }
  59.  
  60.     return tiles;
  61. }
  62. // Shared :: "Legacy/Graphics/Save TIL.js"
  63.  
  64. // Function to save a TIL file with variable bit depth, including support for non-standard bit depths and padding.
  65. function saveTIL(tiles, tileWidth, tileHeight, bitDepth, padPerImage = true) {
  66.     const tileSize = Math.ceil((tileWidth * tileHeight * bitDepth) / 8);
  67.     const numTiles = tiles.length;
  68.     const fileData = new Uint8Array(numTiles * tileSize);
  69.  
  70.     let bitPos = 0;
  71.  
  72.     for (let i = 0; i < numTiles; i++) {
  73.         const tile = tiles[i];
  74.  
  75.         for (let j = 0; j < tileWidth * tileHeight; j++) {
  76.             const byteIndex = Math.floor(bitPos / 8);
  77.             const bitOffset = bitPos % 8;
  78.  
  79.             switch (bitDepth) {
  80.                 case 8:
  81.                     fileData[byteIndex] = tile[j]; // Direct byte-per-pixel mapping.
  82.                     break;
  83.                 case 4:
  84.                     fileData[Math.floor(j / 2)] |= (tile[j] & 0x0F) << ((j % 2) ? 0 : 4);
  85.                     break;
  86.                 case 2:
  87.                     fileData[Math.floor(j / 4)] |= (tile[j] & 0x03) << ((3 - (j % 4)) * 2);
  88.                     break;
  89.                 case 1:
  90.                     fileData[Math.floor(j / 8)] |= (tile[j] & 0x01) << (7 - (j % 8));
  91.                     break;
  92.                 case 3:
  93.                     fileData[byteIndex] |= (tile[j] & 0x07) << (5 - bitOffset);
  94.                     break;
  95.                 case 5:
  96.                     fileData[byteIndex] |= (tile[j] & 0x1F) << (3 - bitOffset);
  97.                     break;
  98.                 case 6:
  99.                     fileData[byteIndex] |= (tile[j] & 0x3F) << (2 - bitOffset);
  100.                     break;
  101.                 case 7:
  102.                     fileData[byteIndex] |= (tile[j] & 0x7F) << (1 - bitOffset);
  103.                     break;
  104.                 default:
  105.                     throw new Error(`Unsupported bit depth: ${bitDepth}`);
  106.             }
  107.  
  108.             bitPos += bitDepth;
  109.         }
  110.  
  111.         // Align to the next byte boundary if padding is applied per image
  112.         if (padPerImage && bitPos % 8 !== 0) {
  113.             bitPos += 8 - (bitPos % 8);
  114.         }
  115.     }
  116.  
  117.     return fileData;
  118. }
  119. // Shared :: "Legacy/Graphics/Load BSV.js"
  120.  
  121. // Function to load a BSV file with variable bit depth (QBASIC BSAVE format) and read dimensions from the header.
  122. function loadBSV(fileData, bitDepth) {
  123.     const headerSize = 7; // Standard BSAVE header size in bytes.
  124.  
  125.     // Read the width and height from the header (bytes 3–6)
  126.     const width = fileData[3] | (fileData[4] << 8);
  127.     const height = fileData[5] | (fileData[6] << 8);
  128.  
  129.     const imageSize = calculateImageSize(bitDepth, width, height);
  130.     const pixelData = fileData.slice(headerSize, headerSize + imageSize);
  131.  
  132.     const pixels = new Uint8Array(width * height);
  133.     let bitPos = 0;
  134.  
  135.     for (let j = 0; j < width * height; j++) {
  136.         const byteIndex = Math.floor(bitPos / 8);
  137.         const bitOffset = bitPos % 8;
  138.  
  139.         switch (bitDepth) {
  140.             case 8:
  141.                 pixels[j] = pixelData[j]; // Direct mapping for 8-bit depth.
  142.                 break;
  143.             case 4:
  144.                 pixels[j] = (pixelData[Math.floor(j / 2)] >> ((j % 2) ? 0 : 4)) & 0x0F;
  145.                 break;
  146.             case 2:
  147.                 pixels[j] = (pixelData[Math.floor(j / 4)] >> ((3 - (j % 4)) * 2)) & 0x03;
  148.                 break;
  149.             case 1:
  150.                 pixels[j] = (pixelData[Math.floor(j / 8)] >> (7 - (j % 8))) & 0x01;
  151.                 break;
  152.             case 3:
  153.                 pixels[j] = (pixelData[byteIndex] >> (5 - bitOffset)) & 0x07;
  154.                 break;
  155.             case 5:
  156.                 pixels[j] = (pixelData[byteIndex] >> (3 - bitOffset)) & 0x1F;
  157.                 break;
  158.             case 6:
  159.                 pixels[j] = (pixelData[byteIndex] >> (2 - bitOffset)) & 0x3F;
  160.                 break;
  161.             case 7:
  162.                 pixels[j] = (pixelData[byteIndex] >> (1 - bitOffset)) & 0x7F;
  163.                 break;
  164.             default:
  165.                 throw new Error(`Unsupported bit depth: ${bitDepth}`);
  166.         }
  167.  
  168.         bitPos += bitDepth;
  169.     }
  170.  
  171.     return { pixels, width, height }; // Return the pixels along with the image dimensions
  172. }
  173. // Shared :: "Legacy/Graphics/Save BSV.js"
  174.  
  175. // Function to save a BSV file with variable bit depth (QBASIC BSAVE format), writing dimensions into the header.
  176. function saveBSV(pixels, bitDepth, width, height) {
  177.     const headerSize = 7; // Standard BSAVE header size.
  178.     const imageSize = calculateImageSize(bitDepth, width, height);
  179.     const fileData = new Uint8Array(headerSize + imageSize);
  180.  
  181.     // Set up the BSAVE header.
  182.     fileData[0] = 0xFD; // 'BSAVE' magic byte.
  183.     fileData[1] = 0x00; // Offset (2 bytes), usually 0x0000 for mode 13h.
  184.     fileData[2] = 0x00;
  185.  
  186.     // Encode width and height in little-endian format
  187.     fileData[3] = width & 0xFF;
  188.     fileData[4] = (width >> 8) & 0xFF;
  189.     fileData[5] = height & 0xFF;
  190.     fileData[6] = (height >> 8) & 0xFF;
  191.  
  192.     let bitPos = 0;
  193.  
  194.     for (let j = 0; j < width * height; j++) {
  195.         const byteIndex = Math.floor(bitPos / 8);
  196.         const bitOffset = bitPos % 8;
  197.  
  198.         switch (bitDepth) {
  199.             case 8:
  200.                 fileData[headerSize + j] = pixels[j]; // Direct byte-per-pixel mapping.
  201.                 break;
  202.             case 4:
  203.                 fileData[Math.floor(j / 2)] |= (pixels[j] & 0x0F) << ((j % 2) ? 0 : 4);
  204.                 break;
  205.             case 2:
  206.                 fileData[Math.floor(j / 4)] |= (pixels[j] & 0x03) << ((3 - (j % 4)) * 2);
  207.                 break;
  208.             case 1:
  209.                 fileData[Math.floor(j / 8)] |= (pixels[j] & 0x01) << (7 - (j % 8));
  210.                 break;
  211.             case 3:
  212.                 fileData[byteIndex] |= (pixels[j] & 0x07) << (5 - bitOffset);
  213.                 break;
  214.             case 5:
  215.                 fileData[byteIndex] |= (pixels[j] & 0x1F) << (3 - bitOffset);
  216.                 break;
  217.             case 6:
  218.                 fileData[byteIndex] |= (pixels[j] & 0x3F) << (2 - bitOffset);
  219.                 break;
  220.             case 7:
  221.                 fileData[byteIndex] |= (pixels[j] & 0x7F) << (1 - bitOffset);
  222.                 break;
  223.             default:
  224.                 throw new Error(`Unsupported bit depth: ${bitDepth}`);
  225.         }
  226.  
  227.         bitPos += bitDepth;
  228.     }
  229.  
  230.     return fileData;
  231. }
  232. // Shared :: "Legacy/Graphics/Support.js"
  233.  
  234. // Function to calculate image size based on bit depth.
  235. function calculateImageSize(bitDepth, width, height) {
  236.     return Math.ceil((width * height * bitDepth) / 8);
  237. }
  238. // Example usage
  239. let tilFile = loadTIL(fileData, 16, 16, 5, true); // Padding between images
  240. let bsvFile = loadBSV(bsvData, 6);
  241. let savedTIL = saveTIL(tilFile, 16, 16, 5, false); // Padding after all images
  242. let savedBSV = saveBSV(bsvFile.pixels, 6, bsvFile.width, bsvFile.height);
  243. // Shared :: "Legacy/Graphics/HuffmanCompression.js"
  244.  
  245. // Function to create a frequency table from the data
  246. function createFrequencyTable(data) {
  247.     let freqTable = {};
  248.     for (let i = 0; i < data.length; i++) {
  249.         let char = data[i];
  250.         freqTable[char] = (freqTable[char] || 0) + 1;
  251.     }
  252.     return freqTable;
  253. }
  254.  
  255. // Function to create Huffman tree from the frequency table
  256. function createHuffmanTree(freqTable) {
  257.     let nodes = Object.entries(freqTable).map(([char, freq]) => ({ char, freq }));
  258.  
  259.     while (nodes.length > 1) {
  260.         nodes.sort((a, b) => a.freq - b.freq);
  261.         let left = nodes.shift();
  262.         let right = nodes.shift();
  263.         let newNode = { char: null, freq: left.freq + right.freq, left, right };
  264.         nodes.push(newNode);
  265.     }
  266.     return nodes[0];
  267. }
  268.  
  269. // Function to generate Huffman codes from the tree
  270. function generateHuffmanCodes(tree, prefix = '', codes = {}) {
  271.     if (tree.char !== null) {
  272.         codes[tree.char] = prefix;
  273.     } else {
  274.         generateHuffmanCodes(tree.left, prefix + '0', codes);
  275.         generateHuffmanCodes(tree.right, prefix + '1', codes);
  276.     }
  277.     return codes;
  278. }
  279.  
  280. // Function to compress data using Huffman codes
  281. function huffmanCompress(data, huffmanCodes) {
  282.     let binaryString = '';
  283.     for (let i = 0; i < data.length; i++) {
  284.         binaryString += huffmanCodes[data[i]];
  285.     }
  286.     let byteArray = [];
  287.     for (let i = 0; i < binaryString.length; i += 8) {
  288.         let byte = binaryString.slice(i, i + 8);
  289.         byteArray.push(parseInt(byte.padEnd(8, '0'), 2)); // Pack into bytes
  290.     }
  291.     return new Uint8Array(byteArray);
  292. }
  293.  
  294. // Function to unpack the compressed byte array into a binary string
  295. function unpackBits(byteArray) {
  296.     let binaryString = '';
  297.     for (let i = 0; i < byteArray.length; i++) {
  298.         let byte = byteArray[i].toString(2).padStart(8, '0');
  299.         binaryString += byte;
  300.     }
  301.     return binaryString;
  302. }
  303.  
  304. // Function to rebuild the Huffman tree from the Huffman codes
  305. function rebuildHuffmanTree(huffmanCodes) {
  306.     let root = {};
  307.     for (let char in huffmanCodes) {
  308.         let code = huffmanCodes[char];
  309.         let node = root;
  310.         for (let bit of code) {
  311.             if (!node[bit]) node[bit] = {};
  312.             node = node[bit];
  313.         }
  314.         node.char = char;
  315.     }
  316.     return root;
  317. }
  318.  
  319. // Function to decode the compressed binary data using the Huffman tree
  320. function huffmanDecompress(binaryString, huffmanTree) {
  321.     let originalData = '';
  322.     let node = huffmanTree;
  323.     for (let bit of binaryString) {
  324.         node = node[bit]; // Traverse the tree
  325.         if (node.char) {
  326.             originalData += node.char;
  327.             node = huffmanTree; // Reset to root
  328.         }
  329.     }
  330.     return originalData;
  331. }
  332. // Function to compress palette and field data using Huffman compression
  333. function compressMasterData(palette, field) {
  334.     // Concatenate the palette and field data as a string
  335.     let combinedData = palette + "fieldStart" + field;
  336.  
  337.     // Create the frequency table
  338.     let freqTable = createFrequencyTable(combinedData);
  339.  
  340.     // Build the Huffman tree
  341.     let huffmanTree = createHuffmanTree(freqTable);
  342.  
  343.     // Generate the Huffman codes
  344.     let huffmanCodes = generateHuffmanCodes(huffmanTree);
  345.  
  346.     // Compress the data
  347.     let compressedData = huffmanCompress(combinedData, huffmanCodes);
  348.  
  349.     return { compressedData, huffmanCodes };
  350. }
  351. // Function to decompress the master data back into palette and field
  352. function decompressMasterData(compressedData, huffmanCodes, width, height, depth = 32) {
  353.     // Unpack the compressed byte array into a binary string
  354.     let binaryString = unpackBits(compressedData);
  355.  
  356.     // Rebuild the Huffman tree from the codes
  357.     let huffmanTree = rebuildHuffmanTree(huffmanCodes);
  358.  
  359.     // Decompress the binary string back into original data
  360.     let originalData = huffmanDecompress(binaryString, huffmanTree);
  361.  
  362.     // Extract the palette and field from the decompressed data
  363.     let paletteLength = originalData.indexOf("fieldStart");
  364.     let palette = originalData.slice(0, paletteLength);
  365.     let field = originalData.slice(paletteLength + "fieldStart".length);
  366.  
  367.     // Rebuild the images from the decompressed palette and field data
  368.     let originalTiles = rebuildImages(palette, field, width, height, depth);
  369.     return originalTiles;
  370. }
  371. // Function to rebuild images from the original data
  372. function rebuildImages(palette, field, width, height, depth = 32) {
  373.     let tiles = [];
  374.     let paletteEntries = [];
  375.  
  376.     // Split the palette into entries
  377.     for (let i = 0; i < palette.length; i += (depth / 4)) {
  378.         paletteEntries.push(palette.slice(i, i + (depth / 4)));
  379.     }
  380.  
  381.     // Reconstruct the tiles
  382.     for (let i = 0; i < field.length / (width * height); i++) {
  383.         let tile = new Uint8Array(width * height);
  384.         for (let j = 0; j < width * height; j++) {
  385.             // Get the index of the color in the palette
  386.             let paletteIndex = parseInt(field.slice((i * width * height + j) * 2, (i * width * height + j) * 2 + 2), 16);
  387.             let color = parseInt(paletteEntries[paletteIndex], 16);
  388.             tile[j] = color;
  389.         }
  390.         tiles.push(tile);
  391.     }
  392.     return tiles;
  393. }
  394. // Function to combine and compress multiple BSV or TIL files
  395. function combineAndCompressFiles(files) {
  396.     let combinedData = {
  397.         palette: "",
  398.         field: "",
  399.         metadata: []
  400.     };
  401.  
  402.     files.forEach(file => {
  403.         let { pixels, width, height, bitDepth, type } = file;
  404.         let { palette, field } = asUnique(asHex(pixels, width, height), width, height);
  405.         combinedData.palette += palette;
  406.         combinedData.field += field;
  407.         combinedData.metadata.push({ width, height, bitDepth, type });
  408.     });
  409.  
  410.     // Compress the combined palette and field data
  411.     return compressMasterData(combinedData.palette, combinedData.field);
  412. }
  413.  
  414. // Function to decompress and reconstruct multiple files
  415. function decompressAndReconstructFiles(compressedData, huffmanCodes, combinedData) {
  416.     let binaryString = unpackBits(compressedData);
  417.     let huffmanTree = rebuildHuffmanTree(huffmanCodes);
  418.     let originalData = huffmanDecompress(binaryString, huffmanTree);
  419.  
  420.     let reconstructedFiles = [];
  421.  
  422.     combinedData.metadata.forEach(metadata => {
  423.         let { width, height, bitDepth, type } = metadata;
  424.         let fieldLength = width * height * 2;
  425.         let field = combinedData.field.slice(0, fieldLength);
  426.         combinedData.field = combinedData.field.slice(fieldLength);
  427.  
  428.         let paletteLength = field.match(/.{1,2}/g).length * (bitDepth / 4);
  429.         let palette = combinedData.palette.slice(0, paletteLength);
  430.         combinedData.palette = combinedData.palette.slice(paletteLength);
  431.  
  432.         let pixels = rebuildImages(palette, field, width, height, bitDepth);
  433.         reconstructedFiles.push({ pixels, width, height, bitDepth, type });
  434.     });
  435.  
  436.     return reconstructedFiles;
  437. }
  438.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement