moebius-web

web based ansi art editor

scripts/loaders.js


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, <pos>. Also advances <pos> 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 <pos> 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 <pos> 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 <num> characters at the current file position, and strips the trailing whitespace characters. Advances <pos> by <num> 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 <pos>, a string of characters matches <match>. Does not increment <pos>.
        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 <num> bytes found at the current <pos>. Also increments <pos>.
        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 <pos>. Equivalent to seeking a file to a new position.
        this.seek = function (newPos) {
            pos = newPos;
        };

        // Returns the value found at <pos>, without incrementing <pos>.
        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. <pos>.
        this.getPos = function () {
            return pos;
        };

        // Returns true if the end of file has been reached. <this.size> is set later by the SAUCE parsing section, as it is not always the same value as the length of <bytes>. (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 <this.sauce.comments> 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 <this.size> 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 <this.size> to the position where the SAUCE record begins.
                this.size = bytes.length - 128;
            }
        } else {
            // If there is no SAUCE record, assume that everything in <bytes> 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 <y>, then <x>.
                            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 <x> and <y> positions.
                            savedX = x;
                            savedY = y;
                            break;
                        case "u": // Restore the current <x> and <y> 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 <compressed>, if non-blink mode is set, <nonBlink>, and if 512 characters are included with the font data. <char512>.
            this.compressed = ((flags & 4) === 4);
            this.nonBlink = ((flags & 8) === 8);
            this.char512 = ((flags & 16) === 16);
        }

        // Routine to decompress data found in an XBin <file>, which contains a Run-Length encoding scheme. Needs to know the current <width> and <height> 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(); // <p>, the current code under inspection.
                count = p & 63; // <count>, 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, <uncompressed> 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
    };
}());

Download

raw zip tar