From 25ce2e3221d0ee240f8dffdda215559d87e3bf92 Mon Sep 17 00:00:00 2001 From: vonmillhausen Date: Mon, 26 Jun 2023 14:11:01 +0100 Subject: [PATCH] Transitioned to shared Javascript library There was a lot of repeated JS across all my tools (e.g., image conversion, BIOS hash checking, etc.), and so I decided to move all shared code out to its own separate library. This will improve scalability and maintainability at the cost of portability - a fair trade in my opinion. This work also involved large-scale refactoring of all of the tools. There's no actual functional difference (all the tools should behave exactly the same), bar some UI improvements for the oldest tools as a result of moving to the same codebase used by the more recent tools. --- tools/biosCRC32Patcher.htm | 58 +-- tools/bootLogoChanger.htm | 397 ++++++----------- tools/buttonMappingChanger.htm | 417 ++++++++---------- tools/genericImageTool.htm | 749 ++++++++------------------------- tools/saveStateTool.htm | 146 ++----- tools/tools.js | 646 ++++++++++++++++++++++++++++ 6 files changed, 1184 insertions(+), 1229 deletions(-) create mode 100644 tools/tools.js diff --git a/tools/biosCRC32Patcher.htm b/tools/biosCRC32Patcher.htm index b9170f1..d5830c2 100644 --- a/tools/biosCRC32Patcher.htm +++ b/tools/biosCRC32Patcher.htm @@ -9,26 +9,31 @@

Data Frog SF2000 BIOS CRC32 Patcher

This tool will recalculate and patch the CRC32 check bits for a modified bisrv.asd BIOS file for the SF2000; only really useful if you've manually modified data outside of the "safe" areas covered by my other tools. If you haven't done your own hex editing of the BIOS, then this tool probably isn't for you! 🙂

-

IMPORTANT NOTE: This tool will just blindly modify bytes 0x18c to 0x18f of whatever input file you provide; correct usage of this tool is therefore up to you, not me! 🤣

+


Step 1: Select Your bisrv.asd

Select your bisrv.asd file. You should probably make a backup of the file first, just in case!

-
+
+
-

CC0: public domain. Version 1.0, 20230526.1

+

CC0: public domain. Version 1.1, 20230626.1

diff --git a/tools/bootLogoChanger.htm b/tools/bootLogoChanger.htm index ae2fdba..c639d79 100644 --- a/tools/bootLogoChanger.htm +++ b/tools/bootLogoChanger.htm @@ -10,31 +10,17 @@

Data Frog SF2000 Boot Logo Changer

This tool can be used to alter the boot logo on an SF2000 handheld gaming console. Please note this tool is provided as-is, and no support will be given if this corrupts your device's bios; make sure you have backups of anything you care about before messing with your device's critical files! 🙂


-
-

Step 1: Select Original bisrv.asd

-

Select the bisrv.asd file whose boot logo you want to modify. You should probably make a backup of the file first, just in case! You can find the bisrv.asd file in the bios folder on your microSD card.

-
- -
-
-
-
-
-

Step 2: Select New Logo Image File

-

Select the image file you want to use as the new boot logo. It must be 256 pixels wide and 100 pixels tall. Only PNG and JPEG image types are supported. For PNG files, transparency is ignored; also, you might want to make sure your PNG image has any embedded gamma or colour profile information removed first using a metadata scrubbing tool, otherwise the colour output from this tool might be incorrect. The image is displayed centred on a black background, so you may want to factor that into your design as well.

-
- -
-
-
-
-
-

Step 3: Download Updated bisrv.asd

-

Click the download button for the updated bisrv.asd file; use it to replace the one in the bios folder on your SF2000's microSD card.

-
- -
-
+
+
+

Step 1: Select Original bisrv.asd

+

Select the bisrv.asd file whose boot logo you want to modify. You should probably make a backup of the file first, just in case! You can find the bisrv.asd file in the bios folder on your microSD card.

+
+
+ +
+
+
+
-

CC0: public domain. Version 1.2, 20230522.1

+

CC0: public domain. Version 1.3, 20230626.1

diff --git a/tools/buttonMappingChanger.htm b/tools/buttonMappingChanger.htm index 889809e..881a0e1 100644 --- a/tools/buttonMappingChanger.htm +++ b/tools/buttonMappingChanger.htm @@ -16,17 +16,19 @@

Step 1: Select bisrv.asd or a game ROM

Select the bisrv.asd (for global device mappings) or game ROM file (for per-game mappings) whose button mappings you want to modify. You can find the bisrv.asd file in the bios folder on your device's microSD card.

-
- -
-
+
+
+ +
+
-

CC0: public domain. Version 1.2, 20230522.1

+

CC0: public domain. Version 1.3, 20230626.1

\ No newline at end of file diff --git a/tools/genericImageTool.htm b/tools/genericImageTool.htm index 05f0504..15f7973 100644 --- a/tools/genericImageTool.htm +++ b/tools/genericImageTool.htm @@ -20,6 +20,7 @@ +
-

CC0: public domain. Version 1.0, 20230524.1

+

CC0: public domain. Version 1.1, 20230626.1

diff --git a/tools/saveStateTool.htm b/tools/saveStateTool.htm index a33221a..6f743dd 100644 --- a/tools/saveStateTool.htm +++ b/tools/saveStateTool.htm @@ -10,7 +10,7 @@

Data Frog SF2000 Save State Tool

The SF2000 supports save states for games, but it bundles the save state data into a custom format including a thumbnail and metadata; this bundle can't be directly used as save state data for other emulator platforms (e.g., Retroarch). This tool allows you to extract raw save-state data and thumbnails from SF2000 save-state bundles, in case you want to use them with the same emulator on another device; it also allows you to take save states from another device, and convert them to a format the SF2000 can parse. Please note this tool is provided as-is; make sure you have backups of anything you care about before potentially replacing any files on your SF2000's microSD card! 🙂

This tool makes use of the pako JavaScript library.

-

NOTE: This tool will not convert save states from one emulator's format to another; it only gives you either the raw save state data the SF2000 generated (when converting from the SF2000), or allows you to take any random file and shove it into the container format the SF2000 uses for save states (when converting to the SF2000)… whether or not the SF2000 is able to understand the file you give it is up to the SF2000, not to me! If you're converting save states to the SF2000, and the SF2000 doesn't work with whatever kind of save state you provided, there's nothing I can do about that.

You can find a reference to all the specific emulators and versions used by the SF2000 here; you'll probably only have success if you're using at least the same emulator, if not also the same version. Also, to have any chance of success, you must be using the exact same ROM on both devices!

+

@@ -23,6 +23,7 @@
+
-

CC0: public domain. Version 1.1, 20230621.1

+

CC0: public domain. Version 1.2, 20230626.1

diff --git a/tools/tools.js b/tools/tools.js new file mode 100644 index 0000000..47bc9cf --- /dev/null +++ b/tools/tools.js @@ -0,0 +1,646 @@ +/* + + Von Millhausen's SF2000 Tools Shared JS Library + =============================================== + + There's a bit of overlap between what my SF2000 tools do (e.g., CRC32 + recalculation is fairly common, etc.), and there's some general functions + that might be useful for future tools as well (e.g., `bisrv.asd` hash + checking), so rather than repeating code in each separate tool, any "shared" + functions will go here. + + Just like the tools themselves, this file should be considered CC0 Public + Domain (https://creativecommons.org/publicdomain/zero/1.0/) + + Version 1.0: Initial version + +*/ + +// This function takes in a Uint8Array object, and patches bytes 0x18c to +// 0x18f with an updated CRC32 calculated on bytes 512 to the end of the +// array. Credit to `bnister` for this code! +function patchCRC32(data) { + var c; + var tabCRC32 = new Int32Array(256); + for (var i = 0; i < 256; i++) { + c = i << 24; + for (var j = 0; j < 8; j++) { + c = c & (1 << 31) ? c << 1 ^ 0x4c11db7 : c << 1; + } + tabCRC32[i] = c; + } + c = ~0; + for (var i = 512; i < data.length; i++) { + c = c << 8 ^ tabCRC32[c >>> 24 ^ data[i]]; + } + data[0x18c] = c & 255; + data[0x18d] = c >>> 8 & 255; + data[0x18e] = c >>> 16 & 255; + data[0x18f] = c >>> 24; +} + +// Returns an SHA-256 hash of a given firmware (ignoring common user changes), +// or returns false on failure... +function getFirmwareHash(data) { + + // Data should be a Uint8Array, which as an object is passed by reference... + // we're going to be manipulating that data before generating our hash, but we + // don't want to modify the original object at all... so we'll create a copy, + // and work only on the copy... + var dataCopy = data.slice(); + + // Only really worthwhile doing this for big bisrv.asd files... + if (dataCopy.length > 12640000) { + + // First, replace CRC32 bits with 00... + dataCopy[396] = 0x00; + dataCopy[397] = 0x00; + dataCopy[398] = 0x00; + dataCopy[399] = 0x00; + + // Next identify the boot logo position, and blank it out too... + var badExceptionOffset = findSequence([0x62, 0x61, 0x64, 0x5F, 0x65, 0x78, 0x63, 0x65, 0x70, 0x74, 0x69, 0x6F, 0x6E, 0x00, 0x00, 0x00], dataCopy); + if (badExceptionOffset > -1) { + var bootLogoStart = badExceptionOffset + 16; + for (var i = bootLogoStart; i < (bootLogoStart + 204800); i++) { + dataCopy[i] = 0x00; + } + } + else { + return false; + } + + // Next identify the emulator button mappings (if they exist), and blank + // them out too... + var preButtonMapOffset = findSequence([0x00, 0x00, 0x00, 0x71, 0xDB, 0x8E, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00], dataCopy); + if (preButtonMapOffset > -1) { + var postButtonMapOffset = findSequence([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00], dataCopy, preButtonMapOffset); + if (postButtonMapOffset > -1) { + for (var i = preButtonMapOffset + 16; i < postButtonMapOffset; i++) { + dataCopy[i] = 0x00; + } + } + else { + return false; + } + } + else { + return false; + } + + // If we're here, we've zeroed-out all of the bits of the firmware that are + // semi-user modifiable (boot logo, button mappings and the CRC32 bits); now + // we can generate a hash of what's left and compare it against some known + // values... + return crypto.subtle.digest("SHA-256", dataCopy.buffer) + .then(function(digest) { + var array = Array.from(new Uint8Array(digest)); + var hash = array.map(byte => ("00" + byte.toString(16)).slice(-2)).join(""); + return hash; + }) + .catch(function(error) { + return false; + }); + } + else { + return false; + } +} + +// This function searches for array needle in array haystack starting at offset +// and returns the zero-based index of the first match found, or -1 if not +// found... +function findSequence(needle, haystack, offset) { + + // If offset is not provided, default to 0... + offset = offset || 0; + + // Loop through the haystack array starting from the offset... + for (var i = offset; i < haystack.length - needle.length + 1; i++) { + + // Assume a match until proven otherwise... + var match = true; + + // Loop through the needle array and compare each byte... + for (var j = 0; j < needle.length; j++) { + + if (haystack[i + j] !== needle[j]) { + // Mismatch found, break the inner loop and continue the outer loop... + match = false; + break; + } + + } + + // If match is still true after the inner loop, we have found a match; + // return the index of the start of the match... + if (match) { + return i; + } + } + + // If we reach this point, no match was found... + return -1; +} + +// Generic download function, for sending data to the user's browser as a +// download... +function downloadToBrowser(data, type, name) { + // Send the data to the user's browser as a file download... + var link = document.createElement("a"); + link.href = window.URL.createObjectURL(new Blob([data], {type: type})); + link.download = name; + link.style.display = "none"; + document.body.appendChild(link); + link.click(); + window.URL.revokeObjectURL(link.href); + document.body.removeChild(link); +} + +// This simple function takes in a string representing an SHA1 hash, and +// compares it against the known hashes my tools use for the various stock +// firmware versions; returns a string with my internal "MM.DD" firmware naming +// convention, or false if the provided hash doesn't match... +function knownHash(hash) { + switch (hash) { + case "4411143d3030adc442e99f7ac4e7772f300c844bbe10d639702fb3ba049a4ee1": + return "03.15"; + + case "b50e50aa4b1b1d41489586e989f09d47c4e2bc27c072cb0112f83e6bc04e2cca": + return "04.20"; + + case "d878a99d26242836178b452814e916bef532d05acfcc24d71baa31b8b6f38ffd": + return "05.15"; + + case "6aebab0e4da39e0a997df255ad6a1bd12fdd356cdf51a85c614d47109a0d7d07": + return "05.22"; + + default: + return false; + } +} + +// Takes in an ImageData object, and returns a Uint8Array object containing the +// data in little-endian RGB565 format... +function imageDataToRgb565(input) { + + // Loop through the image data, and convert it to little-endian RGB565. First, + // we'll store the raw RGB565-converted integers in an array, one entry per + // pixel... + var intArray = []; + var pixelCount = 0; + for (var i = 0; i < input.data.length; i += 4){ + + // Read in the raw source RGB colours from the image data stream... + var red = input.data[i]; + var green = input.data[i+1]; + var blue = input.data[i+2]; + + // Use some shifting and masking to get a big-endian version of the RGB565 + // colour and store it in our array before moving on... + intArray[pixelCount] = ((red & 248)<<8) + ((green & 252)<<3) + (blue>>3); + pixelCount++; + } + + // Create a data buffer and a data view; we'll use the view to convert our int + // array data to little-endian format (the "true" below) to be stored in the + // buffer... + var buffer = new ArrayBuffer(intArray.length * 2); + var dataView = new DataView(buffer); + for (var i = 0; i < intArray.length; i++) { + dataView.setInt16(i * 2, intArray[i], true); + } + + // Use the buffer to fill a Uint8Array, which we'll return... + return new Uint8Array(buffer); +} + +// Takes in an ImageData object, and returns a Uint8Array object containing the +// data in BGRA format... +function imageDataToBgra(input) { + + // This is pretty simple - we just loop through the input data (which is in + // RGBA format), and swap the Red and Blue channels to output BGRA instead... + output = new Uint8Array(input.data.length); + for (var i = 0; i < input.data.length; i += 4) { + output[i] = input.data[i + 2]; + output[i + 1] = input.data[i + 1]; + output[i + 2] = input.data[i]; + output[i + 3] = input.data[i + 3]; + } + return output; +} + +// Takes in a Uint8Array object containing little-endian RGB565 image data, a +// width and a height, and outputs an ImageData object... +function rgb565ToImageData(input, width, height) { + + // Create an output ImageData object of the specified dimensions; it'll + // default to transparent black, but we'll fill it with our input data... + output = new ImageData(width, height); + outputIndex = 0; + for (var i = 0; i < input.length; i += 2) { + + // Check to make sure we haven't run out of space in our output buffer... + if (outputIndex < output.data.length) { + + // Read in two bytes, representing one RGB565 pixel in little-endian + // format... + var byte1 = input[i]; + var byte2 = input[i + 1]; + + // Extract the red, green and blue components from them. The first five + // bits of byte2 are red, the first three bits of byte1 and the last + // three bits of byte 2 are green, and the last five bits of byte1 are + // blue... + var red = (byte2 & 0b11111000) >> 3; + var green = ((byte1 & 0b11100000) >> 5) | ((byte2 & 0b00000111) << 3); + var blue = byte1 & 0b00011111; + + // These values are in 5-bit/6-bit ranges; we need to scale them to 8-bit + // ranges for the colours to look right... + red = Math.round(red * 255 / 31); + green = Math.round(green * 255 / 63); + blue = Math.round(blue * 255 / 31); + + // Finally, store the RGB values in our ImageData's data array, being + // sure to set the A component to 255... + output.data[outputIndex] = red; + output.data[outputIndex + 1] = green; + output.data[outputIndex + 2] = blue; + output.data[outputIndex + 3] = 255; + outputIndex += 4; + } + else { + + // Oops, we've run out of room in our output data buffer; no point in + // trying to process any more input data! + break; + } + } + + // If we've run out of input data, but haven't reached the end of our + // ImageData object's buffer, fill the rest of that buffer with white... + while (outputIndex / 4 < width * height) { + output.data[outputIndex] = 255; + output.data[outputIndex + 1] = 255; + output.data[outputIndex + 2] = 255; + output.data[outputIndex + 3] = 255; + outputIndex += 4; + } + + // Finally, return our ImageData object... + return output; +} + +// Takes in a Uint8Array object containing raw BGRA image data, a width and a +// height, and outputs an ImageData object... +function bgraToImageData(input, width, height) { + + // Create an output ImageData object of the specified dimensions; it'll + // default to transparent black, but we'll fill it with our input data... + output = new ImageData(width, height); + outputIndex = 0; + for (var i = 0; i < input.length; i += 4) { + + // Check to make sure we haven't run out of space in our output buffer... + if (outputIndex < output.data.length) { + + // The input data is *nearly* RGBA as it is - it's just that the R and B + // colour channels are swapped... so, just swap them back! + output.data[i] = input[i + 2]; + output.data[i + 1] = input[i + 1]; + output.data[i + 2] = input[i]; + output.data[i + 3] = input[i + 3]; + outputIndex += 4; + } + else { + + // Oops, we've run out of room in our output data buffer; no point in + // trying to process any more input data! + break; + } + } + + // If we've run out of input data, but haven't reached the end of our + // ImageData object's buffer, fill the rest of that buffer with white... + while (outputIndex / 4 < width * height) { + output.data[outputIndex] = 255; + output.data[outputIndex + 1] = 255; + output.data[outputIndex + 2] = 255; + output.data[outputIndex + 3] = 255; + outputIndex += 4; + } + + // Finally, return our ImageData object... + return output; +} + +// This function takes in a Javascript Image object, and outputs it as an +// ImageData object instead... +function imageToImageData(image) { + + // Create a virtual canvas, and load it up with our image file... + var canvas = document.createElement("canvas"); + var context = canvas.getContext("2d"); + canvas.width = image.width; + canvas.height = image.height; + + // Draw our image to the canvas, which will allow us to get data about the + // image... + context.drawImage(image, 0, 0, image.width, image.height); + var imageData = context.getImageData(0, 0, image.width, image.height); + + // Return the ImageData object... + return imageData; +} + +// This function takes in an ImageData object, and returns a new ImageData +// object containing a scaled version of the original image. The new image's +// width, height and scaling method are also specified... +function scaleImage(input, newWidth, newHeight, method) { + + // Utility function which takes in an ImageData object, and returns + // a (partially) upscaled version using bilinear filtering... + function _bilinear(imageData, newWidth, newHeight) { + var canvas = document.createElement("canvas"); + var context = canvas.getContext("2d"); + canvas.width = imageData.width; + canvas.height = imageData.height; + context.putImageData(imageData, 0, 0); + + var outCanvas = document.createElement("canvas"); + var outContext = outCanvas.getContext("2d"); + outContext.imageSmoothingEnabled = true; + outContext.imageSmoothingQuality = "high"; + outCanvas.width = newWidth; + outCanvas.height = newHeight; + outContext.drawImage(canvas, 0, 0, canvas.width, canvas.height, 0, 0, newWidth, newHeight); + + return outContext.getImageData(0, 0, newWidth, newHeight); + } + + // Utility function which takes in an ImageData object, and returns + // a (partially) downscaled version using gaussian resampling... + // [Side note: I asked Bing AI for this function; ain't technology grand!] + function _gaussian(imageData, newWidth, newHeight) { + + // Get the original width and height of the image data + let oldWidth = imageData.width; + let oldHeight = imageData.height; + + // Get the pixel data array of the image data + let oldData = imageData.data; + + // Create a new pixel data array for the scaled image data + let newData = new Uint8ClampedArray(newWidth * newHeight * 4); + + // Calculate the scaling factor along each axis + let scaleX = newWidth / oldWidth; + let scaleY = newHeight / oldHeight; + + // Calculate the radius of the Gaussian kernel based on the scaling factor + let radiusX = Math.ceil(1 / scaleX); + let radiusY = Math.ceil(1 / scaleY); + + // Calculate the size of the Gaussian kernel along each axis + let sizeX = radiusX * 2 + 1; + let sizeY = radiusY * 2 + 1; + + // Create a Gaussian kernel array + let kernel = new Float32Array(sizeX * sizeY); + + // Calculate the standard deviation of the Gaussian distribution based on + // the radius + let sigmaX = radiusX / 3; + let sigmaY = radiusY / 3; + + // Calculate the inverse of the variance of the Gaussian distribution along + // each axis + let invVarX = 1 / (2 * sigmaX * sigmaX); + let invVarY = 1 / (2 * sigmaY * sigmaY); + + // Calculate the normalization factor for the Gaussian kernel + let norm = Math.sqrt(2 * Math.PI * sigmaX * sigmaY); + + // Loop through each element in the Gaussian kernel array + for (let ky = -radiusY; ky <= radiusY; ky++) { + for (let kx = -radiusX; kx <= radiusX; kx++) { + // Calculate the index of the element in the Gaussian kernel array + let k = (ky + radiusY) * sizeX + (kx + radiusX); + + // Calculate the value of the element using the Gaussian formula + kernel[k] = Math.exp(-(kx * kx) * invVarX - (ky * ky) * invVarY) / norm; + } + } + + // Loop through each pixel in the new image data + for (let y = 0; y < newHeight; y++) { + for (let x = 0; x < newWidth; x++) { + // Calculate the corresponding coordinates in the old image data + let oldX = x / scaleX; + let oldY = y / scaleY; + + // Initialize the RGBA values of the pixel to zero + let r7 = 0; + let g7 = 0; + let b7 = 0; + let a7 = 0; + + // Initialize the sum of the kernel values to zero + let sum = 0; + + // Loop through each element in the Gaussian kernel array + for (let ky = -radiusY; ky <= radiusY; ky++) { + for (let kx = -radiusX; kx <= radiusX; kx++) { + // Calculate the index of the element in the Gaussian kernel array + let k = (ky + radiusY) * sizeX + (kx + radiusX); + + // Get the value of the element in the Gaussian kernel array + let w = kernel[k]; + + // Calculate the coordinates of the pixel in the old image data that + // corresponds to this element + let x1 = Math.round(oldX + kx); + let y1 = Math.round(oldY + ky); + + // Clamp the coordinates to the valid range + x1 = Math.max(0, Math.min(x1, oldWidth - 1)); + y1 = Math.max(0, Math.min(y1, oldHeight - 1)); + + // Get the index of the pixel in the old pixel data array + let i1 = (y1 * oldWidth + x1) * 4; + + // Get the RGBA values of the pixel in the old pixel data array + let r1 = oldData[i1]; + let g1 = oldData[i1 + 1]; + let b1 = oldData[i1 + 2]; + let a1 = oldData[i1 + 3]; + + // Multiply the RGBA values by the kernel value and add them to the + // pixel values + r7 += r1 * w; + g7 += g1 * w; + b7 += b1 * w; + a7 += a1 * w; + + // Add the kernel value to the sum + sum += w; + } + } + + // Divide the RGBA values by the sum to get an average value + r7 /= sum; + g7 /= sum; + b7 /= sum; + a7 /= sum; + + // Round the RGBA values to integers + r7 = Math.round(r7); + g7 = Math.round(g7); + b7 = Math.round(b7); + a7 = Math.round(a7); + + // Get the index of the pixel in the new pixel data array + let j = (y * newWidth + x) * 4; + + // Set the RGBA values of the pixel in the new pixel data array + newData[j] = r7; + newData[j + 1] = g7; + newData[j + 2] = b7; + newData[j + 3] = a7; + } + } + + // Create and return a new ImageData object with the new pixel data array + // and dimensions + return new ImageData(newData, newWidth, newHeight); + } + + // Get the original width and height... + var width = input.width; + var height = input.height; + + // Before we consider doing *any* scaling, let's check to make sure the new + // dimensions are different from the old ones; if they're not, there's no + // point in doing any scaling! + if (width == newWidth && height == newHeight) { + return input; + } + + // If we're here, then we're really scaling; the process to follow is + // *heavily* dependent upon the provided method, so let's switch based on + // that... + switch (method) { + + // If the method is "Nearest Neighbour"... + case "Nearest Neighbour": + + // Create a new canvas element to draw the scaled image (we'll use the + // canvas to get our output ImageData object)... + var canvas = document.createElement("canvas"); + var context = canvas.getContext("2d"); + + // Set the canvas size to the new dimensions... + canvas.width = newWidth; + canvas.height = newHeight; + + // Create a new image data object to store the scaled pixel data... + var scaledData = context.createImageData(newWidth, newHeight); + + // Loop through each pixel of the new image... + for (var y = 0; y < newHeight; y++) { + for (var x = 0; x < newWidth; x++) { + + // Calculate the index of the new pixel in the scaled data array... + var index = (y * newWidth + x) * 4; + + // Calculate the x and y coordinates of the corresponding pixel in + // the original image... + var x2 = Math.floor(x * width / newWidth); + var y2 = Math.floor(y * height / newHeight); + + // Calculate the index of the original pixel in the data array... + var index2 = (y2 * width + x2) * 4; + + // Copy the color values from the original pixel to the new pixel... + scaledData.data[index] = input.data[index2]; // Red + scaledData.data[index + 1] = input.data[index2 + 1]; // Green + scaledData.data[index + 2] = input.data[index2 + 2]; // Blue + scaledData.data[index + 3] = input.data[index2 + 3]; // Alpha + } + } + + // Finally, return the scaled ImageData object... + return scaledData; + + // If the method is "Bilinear"... + case "Bilinear": + + // OK, "Bilinear" is a bit of a lie... bilinear filtering is fine when + // you're *upscaling* an image, but if you're *downscaling* an image + // by more than half the original's width/height, then true bilinear + // filtering creates just as much of an aliased mess as nearest + // neighbour filtering. Most image editing apps therefore cheat and + // use a resampling algorithm when downscaling, and bilinear filtering + // when upscaling... so that's what we're going to do here too! We'll + // use gaussian resampling for any image axis that's being downscaled, + // and bilinear for any axis that's being upscaled; this should give + // the user a result that's much closer to what they'd expect to see... + + // Let's see which kind of scaling scenario we're in... + if (newWidth > width && newHeight > height) { + + // All dimensions being upscaled, so we'll use bilinear filtering for + // everything... + return _bilinear(input, newWidth, newHeight); + } + else if (newWidth < width && newHeight < height) { + + // All dimensions being downscaled, so we'll use gaussian resampling + // for everything... + return _gaussian(input, newWidth, newHeight); + } + else { + + // It's a mix! + if (newWidth < width) { + + // Gaussian for width, bilinear for height... + let partial = _gaussian(input, newWidth, height); + return _bilinear(partial, newWidth, newHeight); + } + else if (newHeight < height) { + + // Gaussian for height, bilinear for width... + let partial = _gaussian(input, width, newHeight); + return _bilinear(partial, newWidth, newHeight); + } + } + break; + } +} + +// This utility function is used for adding info, warning and error messages to +// the appropriate spots in my tools. It uses custom-modified versions of the +// SVG Famfamfam Silk icon set via https://github.com/frhun/silk-icon-scalable +function setMessage(type, divID, text) { + var icon; + switch(type) { + case "info": + icon = ''; + break; + + case "warning": + icon = ''; + break; + + case "error": + icon = ''; + break; + } + + document.getElementById(divID).innerHTML = "

" + icon + " " + text + "

"; + + return; +} \ No newline at end of file