moebius-web

web based ansi art editor

moebius-web

public/js/file.js


var Load = (function() {
	"use strict";

	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]);

		this.get = function() {
			if (pos >= bytes.length) {
				throw "Unexpected end of file reached.";
			}
			pos += 1;
			return bytes[pos - 1];
		};

		this.get16 = function() {
			var v;
			v = this.get();
			return v + (this.get() << 8);
		};

		this.get32 = function() {
			var v;
			v = this.get();
			v += this.get() << 8;
			v += this.get() << 16;
			return v + (this.get() << 24);
		};

		this.getC = function() {
			return String.fromCharCode(this.get());
		};

		this.getS = function(num) {
			var string;
			string = "";
			while (num > 0) {
				string += this.getC();
				num -= 1;
			}
			return string.replace(/\s+$/, "");
		};

		this.lookahead = function(match) {
			var i;
			for (i = 0; i < match.length; i += 1) {
				if ((pos + i === bytes.length) || (bytes[pos + i] !== match[i])) {
					break;
				}
			}
			return i === match.length;
		};

		this.read = function(num) {
			var t;
			t = pos;

			num = num || this.size - pos;
			while ((pos += 1) < this.size) {
				num -= 1;
				if (num === 0) {
					break;
				}
			}
			return bytes.subarray(t, pos);
		};

		this.seek = function(newPos) {
			pos = newPos;
		};

		this.peek = function(num) {
			num = num || 0;
			return bytes[pos + num];
		};

		this.getPos = function() {
			return pos;
		};

		this.eof = function() {
			return pos === this.size;
		};

		pos = bytes.length - 128;

		if (this.lookahead(SAUCE_ID)) {
			this.sauce = {};

			this.getS(5);

			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

			this.sauce.comments = [];
			commentCount = this.get(); // unsigned 8-bit
			this.sauce.flags = this.get(); // unsigned 8-bit
			if (commentCount > 0) {

				pos = bytes.length - 128 - (commentCount * 64) - 5;

				if (this.lookahead(COMNT_ID)) {

					this.getS(5);

					while (commentCount > 0) {
						this.sauce.comments.push(this.getS(64));
						commentCount -= 1;
					}
				}
			}
		}

		pos = 0;

		if (this.sauce) {

			if (this.sauce.fileSize > 0 && this.sauce.fileSize < bytes.length) {

				this.size = this.sauce.fileSize;
			} else {

				this.size = bytes.length - 128;
			}
		} else {

			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();

		this.raw = function(bytes) {
			var i, j;
			maxY = Math.ceil(bytes.length / 2 / width);
			imageData = new Uint8Array(width * maxY * 3);
			for (i = 0, j = 0; j < bytes.length; i += 3, j += 2) {
				imageData[i] = bytes[j];
				imageData[i + 1] = bytes[j + 1] & 15;
				imageData[i + 2] = bytes[j + 1] >> 4;
			}
		};

		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;

		this.stripBlinking = function() {
			var i;
			for (i = 2; i < imageData.length; i += 3) {
				if (imageData[i] >= 8) {
					imageData[i] -= 8;
				}
			}
		};
	}

	function loadAnsi(bytes) {
		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 += 1;
			} else {
				y += 1;
			}
		}

		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 = (file.sauce !== undefined) ? file.sauce.tInfo1 : 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 += 1) {
									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 += 1) {
									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, blink ? (background + 8) : background);
							} else {
								imageData.set(x - 1, y - 1 + topOfScreen, code, bold ? (background + 8) : background, blink ? (foreground + 8) : foreground);
							}
							x += 1;
							if (x === columns + 1) {
								newLine();
							}
						}
				}
			}
		}

		return {
			"width": columns,
			"height": imageData.getHeight(),
			"data": imageData.getData(),
			"noblink": file.sauce ? ((file.sauce.flags & 1) === 1) : false,
			"title": file.sauce ? file.sauce.title : "",
			"author": file.sauce ? file.sauce.author : "",
			"group": file.sauce ? file.sauce.group : ""
		};
	}

	function convertData(data) {
		var output = new Uint16Array(data.length / 3);
		for (var i = 0, j = 0; i < data.length; i += 1, j += 3) {
			output[i] = (data[j] << 8) + (data[j + 2] << 4) + data[j + 1];
		}
		return output;
	}

	function bytesToString(bytes, offset, size) {
		var text = "", i;
		for (i = 0; i < size; i++) {
			text += String.fromCharCode(bytes[offset + i]);
		}
		return text;
	}

	function getSauce(bytes, defaultColumnValue) {
		var sauce, fileSize, dataType, columns, rows, flags;

		function removeTrailingWhitespace(text) {
			return text.replace(/\s+$/, "");
		}

		if (bytes.length >= 128) {
			sauce = bytes.slice(-128);
			if (bytesToString(sauce, 0, 5) === "SAUCE" && bytesToString(sauce, 5, 2) === "00") {
				fileSize = (sauce[93] << 24) + (sauce[92] << 16) + (sauce[91] << 8) + sauce[90];
				dataType = sauce[94];
				if (dataType === 5) {
					columns = sauce[95] * 2;
					rows = fileSize / columns / 2;
				} else {
					columns = (sauce[97] << 8) + sauce[96];
					rows = (sauce[99] << 8) + sauce[98];
				}
				flags = sauce[105];
				return {
					"title": removeTrailingWhitespace(bytesToString(sauce, 7, 35)),
					"author": removeTrailingWhitespace(bytesToString(sauce, 42, 20)),
					"group": removeTrailingWhitespace(bytesToString(sauce, 62, 20)),
					"fileSize": (sauce[93] << 24) + (sauce[92] << 16) + (sauce[91] << 8) + sauce[90],
					"columns": columns,
					"rows": rows,
					"iceColours": (flags & 0x01) === 1,
					"letterSpacing": (flags >> 1 & 0x02) === 2
				};
			}
		}
		return {
			"title": "",
			"author": "",
			"group": "",
			"fileSize": bytes.length,
			"columns": defaultColumnValue,
			"rows": undefined,
			"iceColours": false,
			"letterSpacing": false
		};
	}

	function convertUInt8ToUint16(uint8Array, start, size) {
		var i, j;
		var uint16Array = new Uint16Array(size / 2);
		for (i = 0, j = 0; i < size; i += 2, j += 1) {
			uint16Array[j] = (uint8Array[start + i] << 8) + uint8Array[start + i + 1];
		}
		return uint16Array;
	}

	function loadBin(bytes) {
		var sauce = getSauce(bytes, 160);
		var data;
		if (sauce.rows === undefined) {
			sauce.rows = sauce.fileSize / 160 / 2;
		}
		data = convertUInt8ToUint16(bytes, 0, sauce.columns * sauce.rows * 2);
		return {
			"columns": sauce.columns,
			"rows": sauce.rows,
			"data": data,
			"iceColours": sauce.iceColours,
			"letterSpacing": sauce.letterSpacing,
			"title": sauce.title,
			"author": sauce.author,
			"group": sauce.group
		};
	}

	function uncompress(bytes, dataIndex, fileSize, column, rows) {
		var data = new Uint16Array(column * rows);
		var i, value, count, j, k, char, attribute;
		for (i = dataIndex, j = 0; i < fileSize;) {
			value = bytes[i++];
			count = value & 0x3F;
			switch (value >> 6) {
				case 1:
					char = bytes[i++];
					for (k = 0; k <= count; k++) {
						data[j++] = (char << 8) + bytes[i++];
					}
					break;
				case 2:
					attribute = bytes[i++];
					for (k = 0; k <= count; k++) {
						data[j++] = (bytes[i++] << 8) + attribute;
					}
					break;
				case 3:
					char = bytes[i++];
					attribute = bytes[i++];
					for (k = 0; k <= count; k++) {
						data[j++] = (char << 8) + attribute;
					}
					break;
				default:
					for (k = 0; k <= count; k++) {
						data[j++] = (bytes[i++] << 8) + bytes[i++];
					}
					break;
			}
		}
		return data;
	}

	function loadXBin(bytes) {
		var sauce = getSauce(bytes);
		var columns, rows, fontHeight, flags, paletteFlag, fontFlag, compressFlag, iceColoursFlag, font512Flag, dataIndex, data;
		if (bytesToString(bytes, 0, 4) === "XBIN" && bytes[4] === 0x1A) {
			columns = (bytes[6] << 8) + bytes[5];
			rows = (bytes[8] << 8) + bytes[7];
			fontHeight = bytes[9];
			flags = bytes[10];
			paletteFlag = (flags & 0x01) === 1;
			fontFlag = (flags >> 1 & 0x01) === 1;
			compressFlag = (flags >> 2 & 0x01) === 1;
			iceColoursFlag = (flags >> 3 & 0x01) === 1;
			font512Flag = (flags >> 4 & 0x01) === 1;
			dataIndex = 11;
			if (paletteFlag === true) {
				dataIndex += 48;
			}
			if (fontFlag === true) {
				if (font512Flag === true) {
					dataIndex += 512 * fontHeight;
				} else {
					dataIndex += 256 * fontHeight;
				}
			}
			if (compressFlag === true) {
				data = uncompress(bytes, dataIndex, sauce.fileSize, columns, rows);
			} else {
				data = convertUInt8ToUint16(bytes, dataIndex, columns * rows * 2);
			}
		}
		return {
			"columns": columns,
			"rows": rows,
			"data": data,
			"iceColours": iceColoursFlag,
			"letterSpacing": false,
			"title": sauce.title,
			"author": sauce.author,
			"group": sauce.group
		};
	}

	function file(file, callback) {
		var reader = new FileReader();
		reader.addEventListener("load", function(evt) {
			var data = new Uint8Array(reader.result);
			var imageData;
			switch (file.name.split(".").pop().toLowerCase()) {
				case "xb":
					imageData = loadXBin(data);
					callback(imageData.columns, imageData.rows, imageData.data, imageData.iceColours, imageData.letterSpacing);
					break;
				case "bin":
					imageData = loadBin(data);
					callback(imageData.columns, imageData.rows, imageData.data, imageData.iceColours, imageData.letterSpacing);
					break;
				default:
					imageData = loadAnsi(data);
					$("sauce-title").value = imageData.title;
					$("sauce-group").value = imageData.group;
					$("sauce-author").value = imageData.author;
					callback(imageData.width, imageData.height, convertData(imageData.data), imageData.noblink, false);
					break;
			}
		});
		reader.readAsArrayBuffer(file);
	}

	return {
		"file": file
	};
}());

var Save = (function() {
	"use strict";
	function saveFile(bytes, sauce, filename) {
		var outputBytes;
		if (sauce !== undefined) {
			outputBytes = new Uint8Array(bytes.length + sauce.length);
			outputBytes.set(sauce, bytes.length);
		} else {
			outputBytes = new Uint8Array(bytes.length);
		}
		outputBytes.set(bytes, 0);
		var downloadLink = document.createElement("A");
		if ((navigator.userAgent.indexOf("Chrome") === -1) && (navigator.userAgent.indexOf("Safari") !== -1)) {
			var base64String = "";
			for (var i = 0; i < outputBytes.length; i += 1) {
				base64String += String.fromCharCode(outputBytes[i]);
			}
			downloadLink.href = "data:application/octet-stream;base64," + btoa(base64String);
		} else {
			var blob = new Blob([outputBytes], { "type": "application/octet-stream" });
			downloadLink.href = URL.createObjectURL(blob);
		}
		downloadLink.download = filename;
		var clickEvent = document.createEvent("MouseEvent");
		clickEvent.initEvent("click", true, true);
		downloadLink.dispatchEvent(clickEvent);
		window.URL.revokeObjectURL(downloadLink.href);
	}

	function createSauce(datatype, filetype, filesize, doFlagsAndTInfoS) {
		function addText(text, maxlength, index) {
			var i;
			for (i = 0; i < maxlength; i += 1) {
				sauce[i + index] = (i < text.length) ? text.charCodeAt(i) : 0x20;
			}
		}
		var sauce = new Uint8Array(129);
		sauce[0] = 0x1A;
		sauce.set(new Uint8Array([0x53, 0x41, 0x55, 0x43, 0x45, 0x30, 0x30]), 1);
		addText($("sauce-title").value, 35, 8);
		addText($("sauce-author").value, 20, 43);
		addText($("sauce-group").value, 20, 63);
		var date = new Date();
		addText(date.getFullYear().toString(10), 4, 83);
		var month = date.getMonth() + 1;
		addText((month < 10) ? ("0" + month.toString(10)) : month.toString(10), 2, 87);
		var day = date.getDate();
		addText((day < 10) ? ("0" + day.toString(10)) : day.toString(10), 2, 89);
		sauce[91] = filesize & 0xFF;
		sauce[92] = (filesize >> 8) & 0xFF;
		sauce[93] = (filesize >> 16) & 0xFF;
		sauce[94] = filesize >> 24;
		sauce[95] = datatype;
		sauce[96] = filetype;
		var columns = textArtCanvas.getColumns();
		sauce[97] = columns & 0xFF;
		sauce[98] = columns >> 8;
		var rows = textArtCanvas.getRows();
		sauce[99] = rows & 0xFF;
		sauce[100] = rows >> 8;
		sauce[105] = 0;
		if (doFlagsAndTInfoS) {
			var flags = 0;
			if (textArtCanvas.getIceColours() === true) {
				flags += 1;
			}
			if (font.getLetterSpacing() === false) {
				flags += (1 << 1);
			} else {
				flags += (1 << 2);
			}
			sauce[106] = flags;
			var fontName = "IBM VGA";
			addText(fontName, fontName.length, 107);
		}
		return sauce;
	}

	function getUnicode(charCode) {
		switch (charCode) {
			case 1: return 0x263A;
			case 2: return 0x263B;
			case 3: return 0x2665;
			case 4: return 0x2666;
			case 5: return 0x2663;
			case 6: return 0x2660;
			case 7: return 0x2022;
			case 8: return 0x25D8;
			case 9: return 0x25CB;
			case 10: return 0x25D9;
			case 11: return 0x2642;
			case 12: return 0x2640;
			case 13: return 0x266A;
			case 14: return 0x266B;
			case 15: return 0x263C;
			case 16: return 0x25BA;
			case 17: return 0x25C4;
			case 18: return 0x2195;
			case 19: return 0x203C;
			case 20: return 0x00B6;
			case 21: return 0x00A7;
			case 22: return 0x25AC;
			case 23: return 0x21A8;
			case 24: return 0x2191;
			case 25: return 0x2193;
			case 26: return 0x2192;
			case 27: return 0x2190;
			case 28: return 0x221F;
			case 29: return 0x2194;
			case 30: return 0x25B2;
			case 31: return 0x25BC;
			case 127: return 0x2302;
			case 128: return 0x00C7;
			case 129: return 0x00FC;
			case 130: return 0x00E9;
			case 131: return 0x00E2;
			case 132: return 0x00E4;
			case 133: return 0x00E0;
			case 134: return 0x00E5;
			case 135: return 0x00E7;
			case 136: return 0x00EA;
			case 137: return 0x00EB;
			case 138: return 0x00E8;
			case 139: return 0x00EF;
			case 140: return 0x00EE;
			case 141: return 0x00EC;
			case 142: return 0x00C4;
			case 143: return 0x00C5;
			case 144: return 0x00C9;
			case 145: return 0x00E6;
			case 146: return 0x00C6;
			case 147: return 0x00F4;
			case 148: return 0x00F6;
			case 149: return 0x00F2;
			case 150: return 0x00FB;
			case 151: return 0x00F9;
			case 152: return 0x00FF;
			case 153: return 0x00D6;
			case 154: return 0x00DC;
			case 155: return 0x00A2;
			case 156: return 0x00A3;
			case 157: return 0x00A5;
			case 158: return 0x20A7;
			case 159: return 0x0192;
			case 160: return 0x00E1;
			case 161: return 0x00ED;
			case 162: return 0x00F3;
			case 163: return 0x00FA;
			case 164: return 0x00F1;
			case 165: return 0x00D1;
			case 166: return 0x00AA;
			case 167: return 0x00BA;
			case 168: return 0x00BF;
			case 169: return 0x2310;
			case 170: return 0x00AC;
			case 171: return 0x00BD;
			case 172: return 0x00BC;
			case 173: return 0x00A1;
			case 174: return 0x00AB;
			case 175: return 0x00BB;
			case 176: return 0x2591;
			case 177: return 0x2592;
			case 178: return 0x2593;
			case 179: return 0x2502;
			case 180: return 0x2524;
			case 181: return 0x2561;
			case 182: return 0x2562;
			case 183: return 0x2556;
			case 184: return 0x2555;
			case 185: return 0x2563;
			case 186: return 0x2551;
			case 187: return 0x2557;
			case 188: return 0x255D;
			case 189: return 0x255C;
			case 190: return 0x255B;
			case 191: return 0x2510;
			case 192: return 0x2514;
			case 193: return 0x2534;
			case 194: return 0x252C;
			case 195: return 0x251C;
			case 196: return 0x2500;
			case 197: return 0x253C;
			case 198: return 0x255E;
			case 199: return 0x255F;
			case 200: return 0x255A;
			case 201: return 0x2554;
			case 202: return 0x2569;
			case 203: return 0x2566;
			case 204: return 0x2560;
			case 205: return 0x2550;
			case 206: return 0x256C;
			case 207: return 0x2567;
			case 208: return 0x2568;
			case 209: return 0x2564;
			case 210: return 0x2565;
			case 211: return 0x2559;
			case 212: return 0x2558;
			case 213: return 0x2552;
			case 214: return 0x2553;
			case 215: return 0x256B;
			case 216: return 0x256A;
			case 217: return 0x2518;
			case 218: return 0x250C;
			case 219: return 0x2588;
			case 220: return 0x2584;
			case 221: return 0x258C;
			case 222: return 0x2590;
			case 223: return 0x2580;
			case 224: return 0x03B1;
			case 225: return 0x00DF;
			case 226: return 0x0393;
			case 227: return 0x03C0;
			case 228: return 0x03A3;
			case 229: return 0x03C3;
			case 230: return 0x00B5;
			case 231: return 0x03C4;
			case 232: return 0x03A6;
			case 233: return 0x0398;
			case 234: return 0x03A9;
			case 235: return 0x03B4;
			case 236: return 0x221E;
			case 237: return 0x03C6;
			case 238: return 0x03B5;
			case 239: return 0x2229;
			case 240: return 0x2261;
			case 241: return 0x00B1;
			case 242: return 0x2265;
			case 243: return 0x2264;
			case 244: return 0x2320;
			case 245: return 0x2321;
			case 246: return 0x00F7;
			case 247: return 0x2248;
			case 248: return 0x00B0;
			case 249: return 0x2219;
			case 250: return 0x00B7;
			case 251: return 0x221A;
			case 252: return 0x207F;
			case 253: return 0x00B2;
			case 254: return 0x25A0;
			case 0:
			case 255:
				return 0x00A0;
			default:
				return charCode;
		}
	}

	function unicodeToArray(unicode) {
		if (unicode < 0x80) {
			return [unicode];
		} else if (unicode < 0x800) {
			return [(unicode >> 6) | 192, (unicode & 63) | 128];
		}
		return [(unicode >> 12) | 224, ((unicode >> 6) & 63) | 128, (unicode & 63) | 128];
	}

	function getUTF8(charCode) {
		return unicodeToArray(getUnicode(charCode));
	}

	function encodeANSi(useUTF8) {
		function ansiColor(binColor) {
			switch (binColor) {
				case 1:
					return 4;
				case 3:
					return 6;
				case 4:
					return 1;
				case 6:
					return 3;
				default:
					return binColor;
			}
		}
		var imageData = textArtCanvas.getImageData();
		var columns = textArtCanvas.getColumns();
		var rows = textArtCanvas.getRows();
		var output = [27, 91, 48, 109];
		var bold = false;
		var blink = false;
		var currentForeground = 7;
		var currentBackground = 0;
		var currentBold = false;
		var currentBlink = false;
		for (var inputIndex = 0; inputIndex < rows * columns; inputIndex++) {
			var attribs = [];
			var charCode = imageData[inputIndex] >> 8;
			var foreground = imageData[inputIndex] & 15;
			var background = imageData[inputIndex] >> 4 & 15;
			switch (charCode) {
				case 10:
					charCode = 9;
					break;
				case 13:
					charCode = 14;
					break;
				case 26:
					charCode = 16;
					break;
				case 27:
					charCode = 17;
					break;
				default:
					break;
			}
			if (foreground > 7) {
				bold = true;
				foreground = foreground - 8;
			} else {
				bold = false;
			}
			if (background > 7) {
				blink = true;
				background = background - 8;
			} else {
				blink = false;
			}
			if ((currentBold && !bold) || (currentBlink && !blink)) {
				attribs.push([48]);
				currentForeground = 7;
				currentBackground = 0;
				currentBold = false;
				currentBlink = false;
			}
			if (bold && !currentBold) {
				attribs.push([49]);
				currentBold = true;
			}
			if (blink && !currentBlink) {
				attribs.push([53]);
				currentBlink = true;
			}
			if (foreground !== currentForeground) {
				attribs.push([51, 48 + ansiColor(foreground)]);
				currentForeground = foreground;
			}
			if (background !== currentBackground) {
				attribs.push([52, 48 + ansiColor(background)]);
				currentBackground = background;
			}
			if (attribs.length) {
				output.push(27, 91);
				for (var attribIndex = 0; attribIndex < attribs.length; attribIndex += 1) {
					output = output.concat(attribs[attribIndex]);
					if (attribIndex !== attribs.length - 1) {
						output.push(59);
					} else {
						output.push(109);
					}
				}
			}
			if (useUTF8 === true) {
				getUTF8(charCode).forEach((utf8Code) => {
					output.push(utf8Code);
				});
				if ((inputIndex + 1) % columns === 0) {
					output.push(10);
				}
			} else {
				output.push(charCode);
			}
		}
		var sauce = createSauce(1, 1, output.length, true);
		saveFile(new Uint8Array(output), sauce, (useUTF8 === true) ? title.getName() + ".utf8.ans" : title.getName() + ".ans");
	}

	function ans() {
		encodeANSi(false);
	}

	function utf8() {
		encodeANSi(true);
	}

	function convert16BitArrayTo8BitArray(Uint16s) {
		var Uint8s = new Uint8Array(Uint16s.length * 2);
		for (var i = 0, j = 0; i < Uint16s.length; i++, j += 2) {
			Uint8s[j] = Uint16s[i] >> 8;
			Uint8s[j + 1] = Uint16s[i] & 255;
		}
		return Uint8s;
	}

	function bin() {
		var columns = textArtCanvas.getColumns();
		if (columns % 2 === 0) {
			var imageData = convert16BitArrayTo8BitArray(textArtCanvas.getImageData());
			var sauce = createSauce(5, columns / 2, imageData.length, true);
			saveFile(imageData, sauce, title.getName() + ".bin");
		}
	}

	function xb() {
		var imageData = convert16BitArrayTo8BitArray(textArtCanvas.getImageData());
		var columns = textArtCanvas.getColumns();
		var rows = textArtCanvas.getRows();
		var iceColours = textArtCanvas.getIceColours();
		var flags = 0;
		if (iceColours === true) {
			flags += 1 << 3;
		}
		var output = new Uint8Array(11 + imageData.length);
		output.set(new Uint8Array([
			88, 66, 73, 78, 26,
			columns & 255,
			columns >> 8,
			rows & 255,
			rows >> 8,
			font.getHeight(),
			flags
		]), 0);
		output.set(imageData, 11);
		var sauce = createSauce(6, 0, imageData.length, false);
		saveFile(output, sauce, title.getName() + ".xb");
	}

	function dataUrlToBytes(dataURL) {
		var base64Index = dataURL.indexOf(";base64,") + 8;
		var byteChars = atob(dataURL.substr(base64Index, dataURL.length - base64Index));
		var bytes = new Uint8Array(byteChars.length);
		for (var i = 0; i < bytes.length; i++) {
			bytes[i] = byteChars.charCodeAt(i);
		}
		return bytes;
	}

	function png() {
		saveFile(dataUrlToBytes(textArtCanvas.getImage().toDataURL()), undefined, title.getName() + ".png");
	}

	return {
		"ans": ans,
		"utf8": utf8,
		"bin": bin,
		"xb": xb,
		"png": png
	};
}());

Download

raw zip tar