/* Please see this forum post for the reason for this page: https://forum.odroid.com/viewtopic.php?f=159&t=31867&p=235078 ------------------------- An image format was created for the artwork that consists of the following: Width: 2 byte unsigned integer, maximum value 320 (LCD width) Height: 2 byte unsigned integer, maximum value 176 (LCD height - 32 lines on top - 32 lines on bottom) Data: RGB565 (2 bytes) pixel data of Width * Height (All values are little endian) The CRC32 checksum of a file is used as the primary key: /romart/system/index/crc32.art system = nes, gb, gbc, sms, gg, col index = first character of the CRC32 crc32.art = image file as described above with the CRC32 in hexadecimal (including leading zeros) as the name and ".art" as the extension. Currently, only NES has a header (iNES) so the first 16 bytes of a .nes file are skipped when calculating the CRC32. All the other systems do no use a header. (Nes header details: http://nesdev.com/neshdr20.txt ) 0 = 0x4E (N) 1 = 0x45 (E) 2 = 0x53 (S) 3 = 0x1A (Character Break, necessary!) As an example, for a .gbc rom file. First, the CRC32 of the entire file is calculated. In this example we will say its "0x1234abcd". A romart file for this would be located at: /romart/gbc/1/1234ABCD.art */ //############################### var dither_tresshold_r = [ 1, 7, 3, 5, 0, 8, 2, 6, 7, 1, 5, 3, 8, 0, 6, 2, 3, 5, 0, 8, 2, 6, 1, 7, 5, 3, 8, 0, 6, 2, 7, 1, 0, 8, 2, 6, 1, 7, 3, 5, 8, 0, 6, 2, 7, 1, 5, 3, 2, 6, 1, 7, 3, 5, 0, 8, 6, 2, 7, 1, 5, 3, 8, 0 ]; var dither_tresshold_g = [ 1, 3, 2, 2, 3, 1, 2, 2, 2, 2, 0, 4, 2, 2, 4, 0, 3, 1, 2, 2, 1, 3, 2, 2, 2, 2, 4, 0, 2, 2, 0, 4, 1, 3, 2, 2, 3, 1, 2, 2, 2, 2, 0, 4, 2, 2, 4, 0, 3, 1, 2, 2, 1, 3, 2, 2, 2, 2, 4, 0, 2, 2, 0, 4 ]; var dither_tresshold_b = [ 5, 3, 8, 0, 6, 2, 7, 1, 3, 5, 0, 8, 2, 6, 1, 7, 8, 0, 6, 2, 7, 1, 5, 3, 0, 8, 2, 6, 1, 7, 3, 5, 6, 2, 7, 1, 5, 3, 8, 0, 2, 6, 1, 7, 3, 5, 0, 8, 7, 1, 5, 3, 8, 0, 6, 2, 1, 7, 3, 5, 0, 8, 2, 6 ]; let step1DropArea = document.getElementById("step1DropArea"); let step2DropArea = document.getElementById("step2DropArea"); let step1 = document.getElementById("step1"); let step2 = document.getElementById("step2"); let step3 = document.getElementById("step3"); let input1 = document.getElementById("input1"); let input2 = document.getElementById("input2"); let blendCheck = document.getElementById("blending"); let pathInstructionsOut = document.getElementById("pathInstructions"); ;["dragenter", "dragover", "dragleave", "drop"].forEach(eventName => { step1DropArea.addEventListener(eventName, preventDefaults, false); step2DropArea.addEventListener(eventName, preventDefaults, false) }) function preventDefaults (e) { e.preventDefault() e.stopPropagation() } ["dragenter", "dragover"].forEach(eventName => { step1DropArea.addEventListener(eventName, highlight, false); step2DropArea.addEventListener(eventName, highlight, false) }); ["dragleave", "drop"].forEach(eventName => { step1DropArea.addEventListener(eventName, unhighlight, false); step2DropArea.addEventListener(eventName, unhighlight, false) }); function highlight(e) { step1DropArea.classList.add("highlight"); step2DropArea.classList.add("highlight"); } function unhighlight(e) { step1DropArea.classList.remove("highlight"); step2DropArea.classList.remove("highlight"); } step1DropArea.addEventListener("drop", step1HandleDrop, false); step2DropArea.addEventListener("drop", step2HandleDrop, false); function step1HandleDrop(e) { let file = null; if(e[0]) file = e[0][0]; else file = e.dataTransfer.files[0]; let reader = new FileReader(); reader.fileName = file.name; reader.onload = function(e) { let arrayBuffer = reader.result; let array = new Uint8Array(arrayBuffer); let crc32Array = null; if(isNESROM(array)) crc32Result = crc32(array.slice(16)); else crc32Result = crc32(array); updatePathInstructions(file.name, crc32Result); step1.style.display = "none"; step2.style.display = "block"; } reader.readAsArrayBuffer(file); } function isValidExtension(ext) { return ( ext == "bin" || ext == "col" || ext == "gb" || ext == "gbc" || ext == "gg" || ext == "nes" || ext == "sms" ); } function updatePathInstructions(fileNameFull, destName) { let fileExt = fileNameFull.split('.').pop().toLowerCase(); if(isValidExtension(fileExt)) { let system = (fileExt == "bin") ? "col" : fileExt; let index = destName.charAt(0); let path = `<span class="highlightText"><i>[SD-CARD]</i>/romart/${system}/${index}/${destName}.art</span>`; pathInstructionsOut.innerHTML = `<h1>Please copy the downloaded file to:<br />${path}</h1>`; }else pathInstructionsOut.innerHTML = ""; } function step2HandleDrop(e) { let file = null; if(e[0]) file = e[0][0]; else file = e.dataTransfer.files[0]; let reader = new FileReader(); reader.fileName = file.name; reader.onload = function(e) { let arrayBuffer = reader.result; img = document.createElement("img"); fileName = crc32Result + ".art"; img.file = fileName; img.src = arrayBuffer; step2.style.display = "none"; step3.style.display = "block"; if(img.complete) processLargeImage(); else img.addEventListener("load", processLargeImage, false); } reader.readAsDataURL(file); } function processLargeImage() { let imageDataWrapper = convertImageToArray(img); let imageData = imageDataWrapper.data; artImage = toART565(imageData, imageDataWrapper.width, imageDataWrapper.height, ditherOn); let newCanvas = canvasFromART565(artImage); cc.width = newCanvas.width; cc.height = newCanvas.height; cctx.drawImage(newCanvas, 0, 0); } let imageObj = document.getElementById("sourceImage"); let cc = document.getElementById("c"); let cctx = cc.getContext("2d"); let artImage = null; let crc32Result = null; let img = null; let fileName = null; let ditherOn = false; step1.style.display = "block"; function toggleCheck(o) { if(event.currentTarget == event.target) o = o.children[0]; o.checked = !o.checked; changeBlending(); } function changeBlending() { ditherOn = blendCheck.checked; processLargeImage(); } function downloadArtFile(){ saveBlob(artImage, fileName); artImage = null; crc32Result = null; img = null; fileName = null; input1.value = ""; input2.value = ""; step3.style.display = "none"; step1.style.display = "block"; } let saveBlob = (function () { var a = document.createElement("a"); document.body.appendChild(a); a.style = "display: none"; return function (data, fileName) { let blob = new Blob([data], {type: "octet/stream"}); let url = window.URL.createObjectURL(blob); a.href = url; a.download = fileName; a.click(); window.URL.revokeObjectURL(url); }; })(); // https://stackoverflow.com/questions/18638900/javascript-crc32 let crc32 = (function() { var table = new Uint32Array(256); // Pre-generate crc32 polynomial lookup table // http://wiki.osdev.org/CRC32#Building_the_Lookup_Table // ... Actually use Alex's because it generates the correct bit order so no need for the reversal function for(var i = 256; i--;) { var tmp = i; for(var k=8; k--;) { tmp = tmp & 1 ? 3988292384 ^ tmp >>> 1 : tmp >>> 1; } table[i] = tmp; } // crc32b // Example input : [97, 98, 99, 100, 101] (Uint8Array) // Example output : 2240272485 (Uint32) return function(data) { var crc = -1; // Begin with all bits set ( 0xffffffff ) for(var i=0, l=data.length; i<l; i++) { crc = crc >>> 8 ^ table[ crc & 255 ^ data[i] ]; } return ((crc ^ -1) >>> 0).toString(16).toUpperCase().padStart(8, "0"); // Apply binary NOT }; })(); function isNESROM(data) { return ((data[0] == 0x4e) && (data[1] == 0x45) && (data[2] == 0x53) && (data[3] == 0x1a)); } function convertImageToArray(imageObj){ var c = document.createElement("canvas"); var ctx = c.getContext("2d"); ctx.imageSmoothingEnabled = true; let resizedDimensions = calculateAspectRatioFit(imageObj.width, imageObj.height, 320, 176); c.width = resizedDimensions.width; c.height = resizedDimensions.height; ctx.drawImage(imageObj, 0, 0, resizedDimensions.width, resizedDimensions.height); return ctx.getImageData(0, 0, c.width, c.height); } function canvasFromART565(src) { let width = src[1] * 256 + src[0]; let height = src[3] * 256 + src[2]; let imageData = new Uint8ClampedArray(width * height * 4); let srcLength = src.length; var c = document.createElement("canvas"); var ctx = c.getContext("2d"); c.width = width; c.height = height; let destPtr = -1; for(let srcPtr = 0; srcPtr < srcLength; srcPtr += 2 ) { let c = src[srcPtr + 4] + src[srcPtr + 5] * 256; let r = ((c & 0xf800) >> 11) << 3; let g = ((c & 0x7e0) >> 5) << 2; let b = (c & 0x1f) << 3; imageData[++destPtr] = r; imageData[++destPtr] = g; imageData[++destPtr] = b; imageData[++destPtr] = 255; } var newImageData = new ImageData(imageData, width, height); ctx.putImageData(newImageData, 0, 0); return c; } //https://stackoverflow.com/questions/3971841/how-to-resize-images-proportionally-keeping-the-aspect-ratio function calculateAspectRatioFit(srcWidth, srcHeight, maxWidth, maxHeight) { var ratio = Math.min(maxWidth / srcWidth, maxHeight / srcHeight); return { width: srcWidth * ratio, height: srcHeight * ratio }; } //https://stackoverflow.com/questions/11640017/what-is-a-good-optimized-c-c-algorithm-for-converting-a-24-bit-bitmap-to-16-b /* Get 16bit closest color */ function closest_rb(c) { return (c >> 3 << 3); /* red & blue */ } function closest_g(c) { return (c >> 2 << 2); /* green */ } function convertToRGB565(r, g, b) { return (((r >> 3) << 11) | ((g >> 2) << 5) | (b >> 3)); } function dither_888xy565(x, y, r, g, b) { let tresshold_id = ((y & 7) << 3) + (x & 7); let r2 = closest_rb(Math.min(r + dither_tresshold_r[tresshold_id], 0xff)); let g2 = closest_g(Math.min(g + dither_tresshold_g[tresshold_id], 0xff)); let b2 = closest_rb(Math.min(b + dither_tresshold_b[tresshold_id], 0xff)); return convertToRGB565(r2, g2, b2); } function toART565(src, width, height, dither) { let dest = new Uint8ClampedArray((width * height * 2) + 4); dest[0] = width & 0xff; dest[1] = width >> 8; dest[2] = height & 0xff; dest[3] = height >> 8; for(let y = 0; y < height; y++) { for(let x = 0; x < width; x++) { let ptr = y * width + x; let ptrDest = ptr * 4; let ptrSrc = (ptr * 2) + 4; let word16 = null; if(dither) word16 = dither_888xy565(x, y, src[ptrDest], src[ptrDest+ 1], src[ptrDest + 2]); else word16 = convertToRGB565(src[ptrDest], src[ptrDest+ 1], src[ptrDest + 2]); dest[ptrSrc] = word16 & 0xff; dest[ptrSrc + 1] = word16 >> 8; } } return dest; } //############# /* document.querySelector('input').addEventListener('change', function() { var reader = new FileReader(); reader.onload = function() { let arrayBuffer = this.result; let array = new Uint8Array(arrayBuffer); let newCanvas = canvasFromART565(array); cc.width = newCanvas.width; cc.height = newCanvas.height; cctx.drawImage(newCanvas, 0, 0); } reader.readAsArrayBuffer(this.files[0]); }, false); */

!