jspaint/lib/bmp.js

656 lines
22 KiB
JavaScript

/**
* Based on: https://github.com/shaozilee/bmp-js/blob/db2c466ca1869ddc09e4b2143404eb03ecd490db/lib/encoder.js
* @author shaozilee
* @author 1j01
* @license MIT
*
* BMP format encoder, encodes 24bit, 8bit, 4bit, and 1bit BMP files.
* Doesn't support compression.
*
*/
function encodeBMP(imgData, bitsPerPixel = 24) {
if (![1, 4, 8, 24/*, 32*/].includes(bitsPerPixel)) {
throw new Error(`not supported: ${bitsPerPixel} bits per pixel`)
}
const bytesPerPixel = bitsPerPixel / 8; // can be more or less than one
// Rows are always a multiple of 4 bytes long.
const pixelRowSize = Math.ceil(imgData.width * bytesPerPixel / 4) * 4;
const pixelDataSize = imgData.height * pixelRowSize;
const headerInfoSize = 40;
const indexed = bitsPerPixel <= 8;
const maxColorCount = 2 ** bitsPerPixel;
let rgbTable;
let indices;
let colorCount = 0;
if (indexed) {
// rgbTable = [];
// for (const color of palette.slice(0, maxColorCount)) {
// const [r, g, b] = get_rgba_from_color(color);
// rgbTable.push([r, g, b]);
// }
const res = UPNG.quantize(imgData.data, maxColorCount);
indices = res.inds;
rgbTable = res.plte.map((color_entry) => color_entry.est.q.map((component) => Math.round(component * 255)));
colorCount = rgbTable.length;
}
const flag = "BM";
const planes = 1;
const compressionType = 0;// bitsPerPixel === 32 ? 3 : 0
const hr = 0;
const vr = 0;
const importantColorCount = 0; // 0 means all colors are important
const colorTableSize = colorCount * 4;
const offsetToPixelData = 54 + colorTableSize;
const fileSize = pixelDataSize + offsetToPixelData;
const fileArrayBuffer = new ArrayBuffer(fileSize);
const view = new DataView(fileArrayBuffer);
let pos = 0;
// BMP header
view.setUint8(pos, flag.charCodeAt(0)); pos += 1;
view.setUint8(pos, flag.charCodeAt(1)); pos += 1;
view.setUint32(pos, fileSize, true); pos += 4;
pos += 4; // reserved / application-specific
view.setUint32(pos, offsetToPixelData, true); pos += 4;
// DIB header
view.setUint32(pos, headerInfoSize, true); pos += 4;
view.setInt32(pos, imgData.width, true); pos += 4;
view.setInt32(pos, -imgData.height, true); pos += 4; // negative indicates rows are stored top to bottom
view.setUint16(pos, planes, true); pos += 2;
view.setUint16(pos, bitsPerPixel, true); pos += 2;
view.setUint32(pos, compressionType, true); pos += 4;
view.setUint32(pos, pixelDataSize, true); pos += 4;
view.setInt32(pos, hr, true); pos += 4;
view.setInt32(pos, vr, true); pos += 4;
view.setUint32(pos, colorCount, true); pos += 4;
view.setUint32(pos, importantColorCount, true); pos += 4;
if (rgbTable) {
for (const [r, g, b] of rgbTable) {
view.setUint8(pos, b); pos += 1;
view.setUint8(pos, g); pos += 1;
view.setUint8(pos, r); pos += 1;
pos += 1;
}
}
const getColorIndex = (imgDataIndex) => {
return indices[imgDataIndex / 4];
};
let i = 0;
for (let y = 0; y < imgData.height; y += 1) {
for (let x = 0; x < imgData.width;) {
const pixelGroupPos = pos + y * pixelRowSize + x * bytesPerPixel;
if (bitsPerPixel === 1) {
let byte = 0;
for (let j = 0; j < 8 && x + j < imgData.width; j += 1) {
byte |= getColorIndex(i) << (7 - j);
i += 4;
}
view.setUint8(pixelGroupPos, byte);
x += 8;
} else if (bitsPerPixel === 4) {
let byte = 0;
for (let j = 0; j < 2 && x + j < imgData.width; j += 1) {
byte |= getColorIndex(i) << (4 * (1 - j));
i += 4;
}
view.setUint8(pixelGroupPos, byte);
x += 2;
} else if (bitsPerPixel === 8) {
view.setUint8(pixelGroupPos, getColorIndex(i));
i += 4;
x += 1;
} else {
view.setUint8(pixelGroupPos + 2, imgData.data[i + 0]); // red
view.setUint8(pixelGroupPos + 1, imgData.data[i + 1]); // green
view.setUint8(pixelGroupPos + 0, imgData.data[i + 2]); // blue
// if (bitsPerPixel === 32) {
// view.setUint8(pixelGroupPos + 3, imgData.data[i + 3]); // alpha
// }
i += 4;
x += 1;
}
}
}
return view.buffer;
}
/**
* Based on https://github.com/ericandrewlewis/bitmap-js/blob/c33a6137829b18e3420a763ef20bffae874610b3/index.js
* @author ericandrewlewis
* @license MIT
*/
/*
function decodeBMP(arrayBuffer) {
function readBitmapFileHeader(view) {
if (view.getUint8(0) !== "B".charCodeAt(0) || view.getUint8(1) !== "M".charCodeAt(0)) {
throw new Error("not a BMP file"); // Note: exact error message is checked for to detect this case
}
return {
filesize: view.getUint32(2, true),
imageDataOffset: view.getUint32(10, true)
};
}
const dibHeaderLengthToVersionMap = {
12: "BITMAPCOREHEADER",
16: "OS22XBITMAPHEADER",
40: "BITMAPINFOHEADER",
52: "BITMAPV2INFOHEADER",
56: "BITMAPV3INFOHEADER",
64: "OS22XBITMAPHEADER",
108: "BITMAPV4HEADER",
124: "BITMAPV5HEADER"
};
function readDibHeader(view) {
const dibHeaderLength = view.getUint32(14, true);
const header = {};
header.headerLength = dibHeaderLength;
header.headerType = dibHeaderLengthToVersionMap[dibHeaderLength];
header.width = view.getInt32(18, true);
header.height = view.getInt32(22, true); // Note: negative is used to mean rows go top to bottom instead of bottom to top
if (header.headerType == "BITMAPCOREHEADER") {
return header;
}
header.bitsPerPixel = view.getUint16(28, true);
header.compressionType = view.getUint32(30, true);
if (header.headerType == "OS22XBITMAPHEADER") {
return header;
}
header.bitmapDataSize = view.getUint32(34, true);
header.numberOfColorsInPalette = view.getUint32(46, true);
header.numberOfImportantColors = view.getUint32(50, true);
if (header.headerType == "BITMAPINFOHEADER") {
return header;
}
// There are more data fields in later versions of the dib header.
// I hear that BITMAPINFOHEADER is the most widely supported
// header type, so I'm not going to implement them yet.
return header;
}
function readColorTable(view) {
const dibHeader = readDibHeader(view);
const colorTable = [];
const sourceStart = 14 + dibHeader.headerLength;
const numberOfColorsInPalette = dibHeader.numberOfColorsInPalette || (dibHeader.bitsPerPixel <= 8 ? 2 ** dibHeader.bitsPerPixel : 0);
for (let i = 0; i < numberOfColorsInPalette; i += 1) {
colorTable.push({
r: view.getUint8(sourceStart + i * 4 + 2),
g: view.getUint8(sourceStart + i * 4 + 1),
b: view.getUint8(sourceStart + i * 4 + 0),
});
}
return colorTable;
}
const view = new DataView(arrayBuffer);
const fileHeader = readBitmapFileHeader(view);
const dibHeader = readDibHeader(view);
// const imageDataLength = dibHeader.bitmapDataSize;
// const imageDataOffset = fileHeader.imageDataOffset;
const colorTable = readColorTable(view);
// view.copy(imageData, 0, imageDataOffset);
const width = Math.abs(fileHeader.width);
const height = Math.abs(fileHeader.height); // negative is used to mean rows go top to bottom instead of bottom to top
// const imageData = new ImageData(width, height);
// const pixelRowSize = Math.ceil(width * dibHeader.bitsPerPixel / 8 / 4) * 4;
// for (let y = 0; y < height; y += 1) {
// for (let x = 0; x < width; x += 1) {
// const byte = view.readUint8(y * pixelRowSize + x * dibHeader.bitsPerPixel / 8);
// imageData.data[y * height * 4 + 0,1,2,3] = ...;
// }
// }
return {
// width,
// height,
// fileHeader,
// dibHeader,
// imageData,
colorTable,
bitsPerPixel: dibHeader.bitsPerPixel,
};
}
*/
function decodeBMP(arrayBuffer) {
const decoder = new BmpDecoder(arrayBuffer, {toRGBA: true});
return {
bitsPerPixel: decoder.bitsPerPixel,
colorTable: decoder.palette ? decoder.palette.map(({red, green, blue})=> ({r: red, g: green, b: blue})) : [],
imageData: new ImageData(decoder.data, decoder.width, decoder.height),
};
}
/**
* Based on: https://github.com/hipstersmoothie/bmp-ts
* @license MIT
*/
const HeaderTypes = {};
HeaderTypes[HeaderTypes["BITMAP_INFO_HEADER"] = 40] = "BITMAP_INFO_HEADER";
HeaderTypes[HeaderTypes["BITMAP_V2_INFO_HEADER"] = 52] = "BITMAP_V2_INFO_HEADER";
HeaderTypes[HeaderTypes["BITMAP_V3_INFO_HEADER"] = 56] = "BITMAP_V3_INFO_HEADER";
HeaderTypes[HeaderTypes["BITMAP_V4_HEADER"] = 108] = "BITMAP_V4_HEADER";
HeaderTypes[HeaderTypes["BITMAP_V5_HEADER"] = 124] = "BITMAP_V5_HEADER";
Object.freeze(HeaderTypes);
// We have these:
//
// const sample = 0101 0101 0101 0101
// const mask = 0111 1100 0000 0000
// 256 === 0000 0001 0000 0000
//
// We want to take the sample and turn it into an 8-bit value.
//
// 1. We extract the last bit of the mask:
//
// 0000 0100 0000 0000
// ^
//
// Like so:
//
// const a = ~mask = 1000 0011 1111 1111
// const b = a + 1 = 1000 0100 0000 0000
// const c = b & mask = 0000 0100 0000 0000
//
// 2. We shift it to the right and extract the bit before the first:
//
// 0000 0000 0010 0000
// ^
//
// Like so:
//
// const d = mask / c = 0000 0000 0001 1111
// const e = mask + 1 = 0000 0000 0010 0000
//
// 3. We apply the mask and the two values above to a sample:
//
// const f = sample & mask = 0101 0100 0000 0000
// const g = f / c = 0000 0000 0001 0101
// const h = 256 / e = 0000 0000 0000 0100
// const i = g * h = 0000 0000 1010 1000
// ^^^^ ^
//
// Voila, we have extracted a sample and "stretched" it to 8 bits. For samples
// which are already 8-bit, h === 1 and g === i.
function maskColor(maskRed, maskGreen, maskBlue, maskAlpha) {
const maskRedR = (~maskRed + 1) & maskRed;
const maskGreenR = (~maskGreen + 1) & maskGreen;
const maskBlueR = (~maskBlue + 1) & maskBlue;
const maskAlphaR = (~maskAlpha + 1) & maskAlpha;
const shiftedMaskRedL = maskRed / maskRedR + 1;
const shiftedMaskGreenL = maskGreen / maskGreenR + 1;
const shiftedMaskBlueL = maskBlue / maskBlueR + 1;
const shiftedMaskAlphaL = maskAlpha / maskAlphaR + 1;
return {
shiftRed: (x) => (((x & maskRed) / maskRedR) * 0x100) / shiftedMaskRedL,
shiftGreen: (x) => (((x & maskGreen) / maskGreenR) * 0x100) / shiftedMaskGreenL,
shiftBlue: (x) => (((x & maskBlue) / maskBlueR) * 0x100) / shiftedMaskBlueL,
shiftAlpha: maskAlpha !== 0
? (x) => (((x & maskAlpha) / maskAlphaR) * 0x100) / shiftedMaskAlphaL
: () => 255
};
}
class BmpDecoder {
constructor(arrayBuffer, { toRGBA } = { toRGBA: false }) {
this.view = new DataView(arrayBuffer);
this.toRGBA = !!toRGBA;
this.pos = 0;
this.bottomUp = true;
this.flag = String.fromCharCode(this.view.getUint8(0)) + String.fromCharCode(this.view.getUint8(1));
this.pos += 2;
if (this.flag !== 'BM') {
throw new Error('Invalid BMP File');
}
this.locRed = this.toRGBA ? 0 : 3;
this.locGreen = this.toRGBA ? 1 : 2;
this.locBlue = this.toRGBA ? 2 : 1;
this.locAlpha = this.toRGBA ? 3 : 0;
this.parseHeader();
this.parseRGBA();
}
parseHeader() {
this.fileSize = this.view.getUint32(this.pos, true); this.pos += 4;
this.reserved1 = this.view.getUint16(this.pos, true); this.pos += 2;
this.reserved2 = this.view.getUint16(this.pos, true); this.pos += 2;
this.offset = this.view.getUint32(this.pos, true); this.pos += 4;
// End of BITMAP_FILE_HEADER
this.headerSize = this.view.getUint32(this.pos, true); this.pos += 4;
if (!(this.headerSize in HeaderTypes)) {
throw new Error(`Unsupported BMP header size ${this.headerSize}`);
}
this.width = this.view.getInt32(this.pos, true); this.pos += 4;
this.height = this.view.getInt32(this.pos, true); this.pos += 4;
if (this.height < 0) {
this.height *= -1;
this.bottomUp = false;
}
this.planes = this.view.getUint16(this.pos, true); this.pos += 2;
this.bitsPerPixel = this.view.getUint16(this.pos, true); this.pos += 2;
this.compression = this.view.getUint32(this.pos, true); this.pos += 4;
this.rawSize = this.view.getUint32(this.pos, true); this.pos += 4;
this.hr = this.view.getInt32(this.pos, true); this.pos += 4;
this.vr = this.view.getInt32(this.pos, true); this.pos += 4;
this.colors = this.view.getUint32(this.pos, true); this.pos += 4;
this.importantColors = this.view.getUint32(this.pos, true); this.pos += 4;
// De facto defaults
if (this.bitsPerPixel === 32) {
this.maskAlpha = 0;
this.maskRed = 0x00ff0000;
this.maskGreen = 0x0000ff00;
this.maskBlue = 0x000000ff;
}
else if (this.bitsPerPixel === 16) {
this.maskAlpha = 0;
this.maskRed = 0x7c00;
this.maskGreen = 0x03e0;
this.maskBlue = 0x001f;
}
// End of BITMAP_INFO_HEADER
if (this.headerSize > HeaderTypes.BITMAP_INFO_HEADER ||
this.compression === 3 /* BI_BIT_FIELDS */ ||
this.compression === 6 /* BI_ALPHA_BIT_FIELDS */) {
this.maskRed = this.view.getUint32(this.pos, true); this.pos += 4;
this.maskGreen = this.view.getUint32(this.pos, true); this.pos += 4;
this.maskBlue = this.view.getUint32(this.pos, true); this.pos += 4;
}
// End of BITMAP_V2_INFO_HEADER
if (this.headerSize > HeaderTypes.BITMAP_V2_INFO_HEADER ||
this.compression === 6 /* BI_ALPHA_BIT_FIELDS */) {
this.maskAlpha = this.view.getUint32(this.pos, true); this.pos += 4;
}
// End of BITMAP_V3_INFO_HEADER
if (this.headerSize > HeaderTypes.BITMAP_V3_INFO_HEADER) {
this.pos +=
HeaderTypes.BITMAP_V4_HEADER - HeaderTypes.BITMAP_V3_INFO_HEADER;
}
// End of BITMAP_V4_HEADER
if (this.headerSize > HeaderTypes.BITMAP_V4_HEADER) {
this.pos += HeaderTypes.BITMAP_V5_HEADER - HeaderTypes.BITMAP_V4_HEADER;
}
// End of BITMAP_V5_HEADER
if (this.bitsPerPixel <= 8 || this.colors > 0) {
const len = this.colors === 0 ? 1 << this.bitsPerPixel : this.colors;
this.palette = new Array(len);
for (let i = 0; i < len; i++) {
const blue = this.view.getUint8(this.pos); this.pos += 1;
const green = this.view.getUint8(this.pos); this.pos += 1;
const red = this.view.getUint8(this.pos); this.pos += 1;
const quad = this.view.getUint8(this.pos); this.pos += 1;
this.palette[i] = {
red,
green,
blue,
quad
};
}
}
// End of color table
const coloShift = maskColor(this.maskRed, this.maskGreen, this.maskBlue, this.maskAlpha);
this.shiftRed = coloShift.shiftRed;
this.shiftGreen = coloShift.shiftGreen;
this.shiftBlue = coloShift.shiftBlue;
this.shiftAlpha = coloShift.shiftAlpha;
}
parseRGBA() {
this.data = new Uint8ClampedArray(this.width * this.height * 4);
switch (this.bitsPerPixel) {
case 1:
this.parse1bpp();
break;
case 4:
this.parse4bpp();
break;
case 8:
this.parse8bpp();
break;
case 16:
this.parse16bpp();
break;
case 24:
this.parse24bpp();
break;
default:
this.parse32bpp();
}
}
parse1bpp() {
const xLen = Math.ceil(this.width / 8);
const mode = xLen % 4;
const padding = mode !== 0 ? 4 - mode : 0;
let lastLine;
this.scanImage(padding, xLen, (x, line) => {
if (line !== lastLine) {
lastLine = line;
}
const b = this.view.getUint8(this.pos); this.pos += 1;
const location = line * this.width * 4 + x * 8 * 4;
for (let i = 0; i < 8; i++) {
if (x * 8 + i < this.width) {
// @TODO: use setPixelData?
const rgb = this.palette[(b >> (7 - i)) & 0x1];
this.data[location + i * 4 + this.locAlpha] = 255;
this.data[location + i * 4 + this.locBlue] = rgb.blue;
this.data[location + i * 4 + this.locGreen] = rgb.green;
this.data[location + i * 4 + this.locRed] = rgb.red;
}
else {
break;
}
}
});
}
parse4bpp() {
if (this.compression === 2 /* BI_RLE4 */) {
let lowNibble = false; //for all count of pixel
let lines = this.bottomUp ? this.height - 1 : 0;
let location = 0;
while (location < this.data.length) {
const a = this.view.getUint8(this.pos); this.pos += 1;
const b = this.view.getUint8(this.pos); this.pos += 1;
//absolute mode
if (a === 0) {
if (b === 0) {
//line end
lines += this.bottomUp ? -1 : 1;
location = lines * this.width * 4;
lowNibble = false;
continue;
}
if (b === 1) {
// image end
break;
}
if (b === 2) {
// offset x, y
const x = this.view.getUint8(this.pos); this.pos += 1;
const y = this.view.getUint8(this.pos); this.pos += 1;
lines += this.bottomUp ? -y : y;
location += y * this.width * 4 + x * 4;
}
else {
let c = this.view.getUint8(this.pos); this.pos += 1;
for (let i = 0; i < b; i++) {
location = this.setPixelData(location, lowNibble ? c & 0x0f : (c & 0xf0) >> 4);
if (i & 1 && i + 1 < b) {
c = this.view.getUint8(this.pos); this.pos += 1;
}
lowNibble = !lowNibble;
}
if ((((b + 1) >> 1) & 1) === 1) {
this.pos += 1;
}
}
}
else {
//encoded mode
for (let i = 0; i < a; i++) {
location = this.setPixelData(location, lowNibble ? b & 0x0f : (b & 0xf0) >> 4);
lowNibble = !lowNibble;
}
}
}
}
else {
const xLen = Math.ceil(this.width / 2);
const mode = xLen % 4;
const padding = mode !== 0 ? 4 - mode : 0;
this.scanImage(padding, xLen, (x, line) => {
const b = this.view.getUint8(this.pos); this.pos += 1;
const location = line * this.width * 4 + x * 2 * 4;
const first4 = b >> 4;
// @TODO: use setPixelData?
let rgb = this.palette[first4];
this.data[location + this.locAlpha] = 255;
this.data[location + this.locBlue] = rgb.blue;
this.data[location + this.locGreen] = rgb.green;
this.data[location + this.locRed] = rgb.red;
if (x * 2 + 1 >= this.width) {
// throw new Error('Something');
return false;
}
const last4 = b & 0x0f;
// @TODO: use setPixelData?
rgb = this.palette[last4];
this.data[location + 4 + this.locAlpha] = 255;
this.data[location + 4 + this.locBlue] = rgb.blue;
this.data[location + 4 + this.locGreen] = rgb.green;
this.data[location + 4 + this.locRed] = rgb.red;
});
}
}
parse8bpp() {
if (this.compression === 1 /* BI_RLE8 */) {
let lines = this.bottomUp ? this.height - 1 : 0;
let location = 0;
while (location < this.data.length) {
const a = this.view.getUint8(this.pos); this.pos += 1;
const b = this.view.getUint8(this.pos); this.pos += 1;
//absolute mode
if (a === 0) {
if (b === 0) {
//line end
lines += this.bottomUp ? -1 : 1;
location = lines * this.width * 4;
continue;
}
if (b === 1) {
//image end
break;
}
if (b === 2) {
//offset x,y
const x = this.view.getUint8(this.pos); this.pos += 1;
const y = this.view.getUint8(this.pos); this.pos += 1;
lines += this.bottomUp ? -y : y;
location += y * this.width * 4 + x * 4;
}
else {
for (let i = 0; i < b; i++) {
const c = this.view.getUint8(this.pos); this.pos += 1;
location = this.setPixelData(location, c);
}
// why 1 === 1???
// eslint-disable-next-line no-self-compare
const shouldIncrement = b & (1 === 1);
if (shouldIncrement) {
this.pos += 1;
}
}
}
else {
//encoded mode
for (let i = 0; i < a; i++) {
location = this.setPixelData(location, b);
}
}
}
}
else {
const mode = this.width % 4;
const padding = mode !== 0 ? 4 - mode : 0;
this.scanImage(padding, this.width, (x, line) => {
const b = this.view.getUint8(this.pos); this.pos += 1;
const location = line * this.width * 4 + x * 4;
// @TODO: use setPixelData?
if (b < this.palette.length) {
const rgb = this.palette[b];
this.data[location + this.locRed] = rgb.red;
this.data[location + this.locGreen] = rgb.green;
this.data[location + this.locBlue] = rgb.blue;
this.data[location + this.locAlpha] = 0xff;
}
else {
this.data[location] = 0xff;
this.data[location + 1] = 0xff;
this.data[location + 2] = 0xff;
this.data[location + 3] = 0xff;
}
});
}
}
parse16bpp() {
const padding = (this.width % 2) * 2;
this.scanImage(padding, this.width, (x, line) => {
const loc = line * this.width * 4 + x * 4;
const px = this.view.getUint16(this.pos, true); this.pos += 2;
this.data[loc + this.locRed] = this.shiftRed(px);
this.data[loc + this.locGreen] = this.shiftGreen(px);
this.data[loc + this.locBlue] = this.shiftBlue(px);
this.data[loc + this.locAlpha] = 255; //this.shiftAlpha(px); // @TODO??
});
}
parse24bpp() {
const padding = this.width % 4;
this.scanImage(padding, this.width, (x, line) => {
const loc = line * this.width * 4 + x * 4;
const blue = this.view.getUint8(this.pos); this.pos += 1;
const green = this.view.getUint8(this.pos); this.pos += 1;
const red = this.view.getUint8(this.pos); this.pos += 1;
this.data[loc + this.locRed] = red;
this.data[loc + this.locGreen] = green;
this.data[loc + this.locBlue] = blue;
this.data[loc + this.locAlpha] = 255;
});
}
parse32bpp() {
this.scanImage(0, this.width, (x, line) => {
const loc = line * this.width * 4 + x * 4;
const px = this.view.getUint32(this.pos, true); this.pos += 4;
this.data[loc + this.locRed] = this.shiftRed(px);
this.data[loc + this.locGreen] = this.shiftGreen(px);
this.data[loc + this.locBlue] = this.shiftBlue(px);
this.data[loc + this.locAlpha] = this.shiftAlpha(px);
});
}
scanImage(padding = 0, width = this.width, processPixel) {
for (let y = this.height - 1; y >= 0; y--) {
const line = this.bottomUp ? y : this.height - 1 - y;
for (let x = 0; x < width; x++) {
const result = processPixel.call(this, x, line);
if (result === false) {
return;
}
}
this.pos += padding;
}
}
setPixelData(location, rgbIndex) {
const { blue, green, red } = this.palette[rgbIndex];
this.data[location + this.locAlpha] = 255;
this.data[location + this.locBlue] = blue;
this.data[location + this.locGreen] = green;
this.data[location + this.locRed] = red;
return location + 4;
}
}