var Loaders = (function() { "use strict"; var Colors; Colors = (function() { function rgb2xyz(rgb) { var xyz; xyz = rgb.map(function(value) { value = value / 255; return ((value > 0.04045) ? Math.pow((value + 0.055) / 1.055, 2.4) : value / 12.92) * 100; }); return [xyz[0] * 0.4124 + xyz[1] * 0.3576 + xyz[2] * 0.1805, xyz[0] * 0.2126 + xyz[1] * 0.7152 + xyz[2] * 0.0722, xyz[0] * 0.0193 + xyz[1] * 0.1192 + xyz[2] * 0.9505]; } function xyz2lab(xyz) { var labX, labY, labZ; function process(value) { return (value > 0.008856) ? Math.pow(value, 1 / 3) : (7.787 * value) + (16 / 116); } labX = process(xyz[0] / 95.047); labY = process(xyz[1] / 100); labZ = process(xyz[2] / 108.883); return [116 * labY - 16, 500 * (labX - labY), 200 * (labY - labZ)]; } function rgb2lab(rgb) { return xyz2lab(rgb2xyz(rgb)); } function labDeltaE(lab1, lab2) { return Math.sqrt(Math.pow(lab1[0] - lab2[0], 2) + Math.pow(lab1[1] - lab2[1], 2) + Math.pow(lab1[2] - lab2[2], 2)); } function rgbDeltaE(rgb1, rgb2) { return labDeltaE(rgb2lab(rgb1), rgb2lab(rgb2)); } function labCompare(lab, palette) { var i, match, value, lowest; for (i = 0; i < palette.length; ++i) { value = labDeltaE(lab, palette[i]); if (i === 0 || value < lowest) { match = i; lowest = value; } } return match; } return { "rgb2xyz": rgb2xyz, "xyz2lab": xyz2lab, "rgb2lab": rgb2lab, "labDeltaE": labDeltaE, "rgbDeltaE": rgbDeltaE, "labCompare": labCompare }; }()); function srcToImageData(src, callback) { var img; img = new Image(); img.onload = function() { var imgCanvas, imgCtx, imgImageData; imgCanvas = ElementHelper.create("canvas", { "width": img.width, "height": img.height }); imgCtx = imgCanvas.getContext("2d"); imgCtx.drawImage(img, 0, 0); imgImageData = imgCtx.getImageData(0, 0, imgCanvas.width, imgCanvas.height); callback(imgImageData); }; img.src = src; } function rgbaAt(imageData, x, y) { var pos; pos = (y * imageData.width + x) * 4; if (pos >= imageData.length) { return [0, 0, 0, 255]; } return [imageData.data[pos], imageData.data[pos + 1], imageData.data[pos + 2], imageData.data[pos + 3]]; } function loadImg(src, callback, palette, codepage, noblink) { srcToImageData(src, function(imageData) { var imgX, imgY, i, paletteLab, topRGBA, botRGBA, topPal, botPal, data; for (paletteLab = [], i = 0; i < palette.COLORS.length; ++i) { paletteLab[i] = Colors.rgb2lab([palette.COLORS[i][0], palette.COLORS[i][1], palette.COLORS[i][2]]); } data = new Uint8Array(Math.ceil(imageData.height / 2) * imageData.width * 3); for (imgY = 0, i = 0; imgY < imageData.height; imgY += 2) { for (imgX = 0; imgX < imageData.width; imgX += 1) { topRGBA = rgbaAt(imageData, imgX, imgY); botRGBA = rgbaAt(imageData, imgX, imgY + 1); if (topRGBA[3] === 0 && botRGBA[3] === 0) { data[i++] = codepage.NULL; data[i++] = 0; data[i++] = 0; } else { topPal = Colors.labCompare(Colors.rgb2lab(topRGBA), paletteLab); botPal = Colors.labCompare(Colors.rgb2lab(botRGBA), paletteLab); if (topPal === botPal) { data[i++] = codepage.FULL_BLOCK; data[i++] = topPal; data[i++] = 0; } else if (topPal < 8 && botPal >= 8) { data[i++] = codepage.LOWER_HALF_BLOCK; data[i++] = botPal; data[i++] = topPal; } else if ((topPal >= 8 && botPal < 8) || (topPal < 8 && botPal < 8)) { data[i++] = codepage.UPPER_HALF_BLOCK; data[i++] = topPal; data[i++] = botPal; } else if (topRGBA[3] === 0) { data[i++] = codepage.LOWER_HALF_BLOCK; data[i++] = botPal; if (noblink) { data[i++] = topPal; } else { data[i++] = topPal - 8; } } else { data[i++] = codepage.UPPER_HALF_BLOCK; data[i++] = topPal; if (noblink) { data[i++] = botPal; } else { data[i++] = botPal - 8; } } } } } callback({ "width": imageData.width, "height": Math.ceil(imageData.height / 2), "data": data, "alpha": true }); }); } function File(bytes) { var pos, SAUCE_ID, COMNT_ID, commentCount; SAUCE_ID = new Uint8Array([0x53, 0x41, 0x55, 0x43, 0x45]); COMNT_ID = new Uint8Array([0x43, 0x4F, 0x4D, 0x4E, 0x54]); // Returns an 8-bit byte at the current byte position, . Also advances by a single byte. Throws an error if we advance beyond the length of the array. this.get = function() { if (pos >= bytes.length) { throw "Unexpected end of file reached."; } return bytes[pos++]; }; // Same as get(), but returns a 16-bit byte. Also advances by two (8-bit) bytes. this.get16 = function() { var v; v = this.get(); return v + (this.get() << 8); }; // Same as get(), but returns a 32-bit byte. Also advances by four (8-bit) bytes. this.get32 = function() { var v; v = this.get(); v += this.get() << 8; v += this.get() << 16; return v + (this.get() << 24); }; // Exactly the same as get(), but returns a character symbol, instead of the value. e.g. 65 = "A". this.getC = function() { return String.fromCharCode(this.get()); }; // Returns a string of characters at the current file position, and strips the trailing whitespace characters. Advances by by calling getC(). this.getS = function(num) { var string; string = ""; while (num-- > 0) { string += this.getC(); } return string.replace(/\s+$/, ''); }; // Returns "true" if, at the current , a string of characters matches . Does not increment . this.lookahead = function(match) { var i; for (i = 0; i < match.length; ++i) { if ((pos + i === bytes.length) || (bytes[pos + i] !== match[i])) { break; } } return i === match.length; }; // Returns an array of bytes found at the current . Also increments . this.read = function(num) { var t; t = pos; // If num is undefined, return all the bytes until the end of file. num = num || this.size - pos; while (++pos < this.size) { if (--num === 0) { break; } } return bytes.subarray(t, pos); }; // Sets a new value for . Equivalent to seeking a file to a new position. this.seek = function(newPos) { pos = newPos; }; // Returns the value found at , without incrementing . this.peek = function(num) { num = num || 0; return bytes[pos + num]; }; // Returns the the current position being read in the file, in amount of bytes. i.e. . this.getPos = function() { return pos; }; // Returns true if the end of file has been reached. is set later by the SAUCE parsing section, as it is not always the same value as the length of . (In case there is a SAUCE record, and optional comments). this.eof = function() { return pos === this.size; }; // Seek to the position we would expect to find a SAUCE record. pos = bytes.length - 128; // If we find "SAUCE". if (this.lookahead(SAUCE_ID)) { this.sauce = {}; // Read "SAUCE". this.getS(5); // Read and store the various SAUCE values. this.sauce.version = this.getS(2); // String, maximum of 2 characters this.sauce.title = this.getS(35); // String, maximum of 35 characters this.sauce.author = this.getS(20); // String, maximum of 20 characters this.sauce.group = this.getS(20); // String, maximum of 20 characters this.sauce.date = this.getS(8); // String, maximum of 8 characters this.sauce.fileSize = this.get32(); // unsigned 32-bit this.sauce.dataType = this.get(); // unsigned 8-bit this.sauce.fileType = this.get(); // unsigned 8-bit this.sauce.tInfo1 = this.get16(); // unsigned 16-bit this.sauce.tInfo2 = this.get16(); // unsigned 16-bit this.sauce.tInfo3 = this.get16(); // unsigned 16-bit this.sauce.tInfo4 = this.get16(); // unsigned 16-bit // Initialize the comments array. this.sauce.comments = []; commentCount = this.get(); // unsigned 8-bit this.sauce.flags = this.get(); // unsigned 8-bit if (commentCount > 0) { // If we have a value for the comments amount, seek to the position we'd expect to find them... pos = bytes.length - 128 - (commentCount * 64) - 5; // ... and check that we find a COMNT header. if (this.lookahead(COMNT_ID)) { // Read COMNT ... this.getS(5); // ... and push everything we find after that into our array, in 64-byte chunks, stripping the trailing whitespace in the getS() function. while (commentCount-- > 0) { this.sauce.comments.push(this.getS(64)); } } } } // Seek back to the start of the file, ready for reading. pos = 0; if (this.sauce) { // If we have found a SAUCE record, and the fileSize field passes some basic sanity checks... if (this.sauce.fileSize > 0 && this.sauce.fileSize < bytes.length) { // Set to the value set in SAUCE. this.size = this.sauce.fileSize; } else { // If it fails the sanity checks, just assume that SAUCE record can't be trusted, and set to the position where the SAUCE record begins. this.size = bytes.length - 128; } } else { // If there is no SAUCE record, assume that everything in relates to an image. this.size = bytes.length; } } function ScreenData(width) { var imageData, maxY, pos; function binColor(ansiColor) { switch (ansiColor) { case 4: return 1; case 6: return 3; case 1: return 4; case 3: return 6; case 12: return 9; case 14: return 11; case 9: return 12; case 11: return 14; default: return ansiColor; } } this.reset = function() { imageData = new Uint8Array(width * 100 * 3); maxY = 0; pos = 0; }; this.reset(); function extendImageData(y) { var newImageData; newImageData = new Uint8Array(width * (y + 100) * 3 + imageData.length); newImageData.set(imageData, 0); imageData = newImageData; } this.set = function(x, y, charCode, fg, bg) { pos = (y * width + x) * 3; if (pos >= imageData.length) { extendImageData(y); } imageData[pos] = charCode; imageData[pos + 1] = binColor(fg); imageData[pos + 2] = binColor(bg); if (y > maxY) { maxY = y; } }; this.getData = function() { return imageData.subarray(0, width * (maxY + 1) * 3); }; this.getHeight = function() { return maxY + 1; }; this.rowLength = width * 3; } function loadAnsi(bytes, icecolors) { var file, escaped, escapeCode, j, code, values, columns, imageData, topOfScreen, x, y, savedX, savedY, foreground, background, bold, blink, inverse; file = new File(bytes); function resetAttributes() { foreground = 7; background = 0; bold = false; blink = false; inverse = false; } resetAttributes(); function newLine() { x = 1; if (y === 26 - 1) { ++topOfScreen; } else { ++y; } } function setPos(newX, newY) { x = Math.min(columns, Math.max(1, newX)); y = Math.min(26, Math.max(1, newY)); } x = 1; y = 1; topOfScreen = 0; escapeCode = ""; escaped = false; columns = 80; imageData = new ScreenData(columns); function getValues() { return escapeCode.substr(1, escapeCode.length - 2).split(";").map(function(value) { var parsedValue; parsedValue = parseInt(value, 10); return isNaN(parsedValue) ? 1 : parsedValue; }); } while (!file.eof()) { code = file.get(); if (escaped) { escapeCode += String.fromCharCode(code); if ((code >= 65 && code <= 90) || (code >= 97 && code <= 122)) { escaped = false; values = getValues(); if (escapeCode.charAt(0) === "[") { switch (escapeCode.charAt(escapeCode.length - 1)) { case "A": // Up cursor. y = Math.max(1, y - values[0]); break; case "B": // Down cursor. y = Math.min(26 - 1, y + values[0]); break; case "C": // Forward cursor. if (x === columns) { newLine(); } x = Math.min(columns, x + values[0]); break; case "D": // Backward cursor. x = Math.max(1, x - values[0]); break; case "H": // Set the cursor position by calling setPos(), first , then . if (values.length === 1) { setPos(1, values[0]); } else { setPos(values[1], values[0]); } break; case "J": // Clear screen. if (values[0] === 2) { x = 1; y = 1; imageData.reset(); } break; case "K": // Clear until the end of line. for (j = x - 1; j < columns; ++j) { imageData.set(j, y - 1 + topOfScreen, 0, 0); } break; case "m": // Attributes, work through each code in turn. for (j = 0; j < values.length; ++j) { if (values[j] >= 30 && values[j] <= 37) { foreground = values[j] - 30; } else if (values[j] >= 40 && values[j] <= 47) { background = values[j] - 40; } else { switch (values[j]) { case 0: // Reset attributes resetAttributes(); break; case 1: // Bold bold = true; break; case 5: // Blink blink = true; break; case 7: // Inverse inverse = true; break; case 22: // Bold off bold = false; break; case 25: // Blink off blink = false; break; case 27: // Inverse off inverse = false; break; } } } break; case "s": // Save the current and positions. savedX = x; savedY = y; break; case "u": // Restore the current and positions. x = savedX; y = savedY; break; } } escapeCode = ""; } } else { switch (code) { case 10: // Lone linefeed (LF). newLine(); break; case 13: // Carriage Return, and Linefeed (CRLF) if (file.peek() === 0x0A) { file.read(1); newLine(); } break; case 26: // Ignore eof characters until the actual end-of-file, or sauce record has been reached. break; default: if (code === 27 && file.peek() === 0x5B) { escaped = true; } else { if (!inverse) { imageData.set(x - 1, y - 1 + topOfScreen, code, bold ? (foreground + 8) : foreground, (icecolors && blink) ? (background + 8) : background); } else { imageData.set(x - 1, y - 1 + topOfScreen, code, bold ? (background + 8) : background, (icecolors && blink) ? (foreground + 8) : foreground); } if (++x === columns + 1) { newLine(); } } } } } return { "width": columns, "height": imageData.getHeight(), "data": imageData.getData() }; } // A function to parse a sequence of bytes representing an XBiN file format. function loadXbin(bytes) { var file, header, imageData, output, i, j; // This function is called to parse the XBin header. function XBinHeader(file) { var flags; // Look for the magic number, throw an error if not found. if (file.getS(4) !== "XBIN") { throw "File ID does not match."; } if (file.get() !== 26) { throw "File ID does not match."; } // Get the dimensions of the image... this.width = file.get16(); this.height = file.get16(); // ... and the height of the font, if included. this.fontHeight = file.get(); // Sanity check for the font height, throw an error if failed. if (this.fontHeight === 0 || this.fontHeight > 32) { throw "Illegal value for the font height (" + this.fontHeight + ")."; } // Retrieve the flags. flags = file.get(); // Check to see if a palette and font is included. this.palette = ((flags & 1) === 1); this.font = ((flags & 2) === 2); // Sanity check for conflicting information in font settings. if (this.fontHeight !== 16 && !this.font) { throw "A non-standard font size was defined, but no font information was included with the file."; } // Check to see if the image data is , if non-blink mode is set, , and if 512 characters are included with the font data. . this.compressed = ((flags & 4) === 4); this.nonBlink = ((flags & 8) === 8); this.char512 = ((flags & 16) === 16); } // Routine to decompress data found in an XBin , which contains a Run-Length encoding scheme. Needs to know the current and of the image. function uncompress(file, width, height) { var uncompressed, p, repeatAttr, repeatChar, count; // Initialize the data used to store the image, each text character has two bytes, one for the character code, and the other for the attribute. uncompressed = new Uint8Array(width * height * 2); i = 0; while (i < uncompressed.length) { p = file.get(); //

, the current code under inspection. count = p & 63; // , the times data is repeated switch (p >> 6) { // Look at which RLE scheme to use case 1: // Handle repeated character code. for (repeatChar = file.get(), j = 0; j <= count; ++j) { uncompressed[i++] = repeatChar; uncompressed[i++] = file.get(); } break; case 2: // Handle repeated attributes. for (repeatAttr = file.get(), j = 0; j <= count; ++j) { uncompressed[i++] = file.get(); uncompressed[i++] = repeatAttr; } break; case 3: // Handle repeated character code and attributes. for (repeatChar = file.get(), repeatAttr = file.get(), j = 0; j <= count; ++j) { uncompressed[i++] = repeatChar; uncompressed[i++] = repeatAttr; } break; default: // Handle no RLE. for (j = 0; j <= count; ++j) { uncompressed[i++] = file.get(); uncompressed[i++] = file.get(); } } } return uncompressed; // Return the final, data. } // Convert the bytes to a File() object, and reader the settings in the header, by calling XBinHeader(). file = new File(bytes); header = new XBinHeader(file); // If palette information is included, read it immediately after the header, if not, use the default palette used for BIN files. if (header.palette) { file.read(48); } // If font information is included, read it, if not, use the default 80x25 font. if (header.font) { file.read(header.fontHeight * 256); } // Fetch the image data, and uncompress if necessary. imageData = header.compressed ? uncompress(file, header.width, header.height) : file.read(header.width * header.height * 2); output = new Uint8Array(imageData.length / 2 * 3); for (i = 0, j = 0; i < imageData.length; i += 2, j += 3) { output[j] = imageData[i]; output[j + 1] = imageData[i + 1] & 15; output[j + 2] = imageData[i + 1] >> 4; } return { "width": header.width, "height": header.height, "data": output }; } function loadFile(file, callback, palette, codepage, noblink) { var extension, reader; extension = file.name.split(".").pop().toLowerCase(); reader = new FileReader(); reader.onload = function(data) { switch (extension) { case "png": case "gif": case "jpg": case "jpeg": loadImg(data.target.result, callback, palette, codepage, noblink); break; case "xb": callback(loadXbin(new Uint8Array(data.target.result))); break; default: callback(loadAnsi(new Uint8Array(data.target.result))); } }; switch (extension) { case "png": case "gif": case "jpg": case "jpeg": reader.readAsDataURL(file); break; default: reader.readAsArrayBuffer(file); } } return { "loadFile": loadFile }; }());