diff --git a/bitmap-autobots/.eslintignore b/bitmap-autobots/.eslintignore new file mode 100644 index 0000000..c6abca4 --- /dev/null +++ b/bitmap-autobots/.eslintignore @@ -0,0 +1,6 @@ +**/node_modules/* +**/vendor/* +**/*.min.js +**/coverage/* +**/build/* +**/assets/* diff --git a/bitmap-autobots/.eslintrc b/bitmap-autobots/.eslintrc new file mode 100644 index 0000000..c8dfef7 --- /dev/null +++ b/bitmap-autobots/.eslintrc @@ -0,0 +1,23 @@ +{ + "rules": { + "no-console": "off", + "indent": [ "error", 2 ], + "quotes": [ "error", "single" ], + "semi": ["error", "always"], + "linebreak-style": [ "error", "unix" ] + }, + "env": { + "es6": true, + "node": true, + "mocha": true, + "jasmine": true + }, + "parserOptions": { + "ecmaFeatures": { + "modules": true, + "experimentalObjectRestSpread": true, + "impliedStrict": true + } + }, + "extends": "eslint:recommended" +} diff --git a/bitmap-autobots/.gitignore b/bitmap-autobots/.gitignore new file mode 100644 index 0000000..b232c07 --- /dev/null +++ b/bitmap-autobots/.gitignore @@ -0,0 +1,138 @@ +# Created by https://www.gitignore.io/api/osx,vim,node,macos,windows + +### macOS ### +*.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### Node ### +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (http://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Typescript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + + +### OSX ### + +# Icon must end with two \r + +# Thumbnails + +# Files that might appear in the root of a volume + +# Directories potentially created on remote AFP share + +### Vim ### +# swap +[._]*.s[a-v][a-z] +[._]*.sw[a-p] +[._]s[a-v][a-z] +[._]sw[a-p] +# session +Session.vim +# temporary +.netrwhist +*~ +# auto-generated tag files +tags + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +ehthumbs.db +ehthumbs_vista.db + +# Folder config file +Desktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.gitignore.io/api/osx,vim,node,macos,windows + +output diff --git a/bitmap-autobots/README.md b/bitmap-autobots/README.md new file mode 100644 index 0000000..f5ecccd --- /dev/null +++ b/bitmap-autobots/README.md @@ -0,0 +1,49 @@ +# Bitmap Transformer + +This project contains five modules which facilitate the transformation of bitmap files: + +* **Color**: Handles color calcuations, parsing, and stringifying. +* **Color Transforms**: Attaches various color transformations to the Color prototype. +* **Bitmap**: Handles parsing of bitmap files. +* **Bitmap Transformer**: Exposes methods for manipulating bitmaps. +* **Bitmap Transforms**: Attaches various bitmap transformations to the Bitmap prototype. + +The available transformations are: +* Black and White +* Grayscale +* Sepia +* Horizontal Flip +* Vertical Flip +* Color Inversion +* Clockwise Rotation +* Counterclockwise Rotation +* Hue Shift +* Lightness Shift +* Saturation Shift +* Redness Shift +* Greenness Shift +* Blueness Shift + +## CLI + +To use the CLI, type `node index.js [filename] [transformation[:parameter]] [transformation[:parameter]] [transformation[:parameter]]` etc. + +The transformation arguments are: +* `bw` -- Black and White +* `gray` -- Grayscale +* `sepia` -- Sepia +* `hflip` -- Horizontal Flip +* `vflip` -- Vertical Flip +* `invert` -- Color Inversion +* `rotc` -- Clockwise Rotation +* `rotcc` -- Counterclockwise Rotation +* `hshift:degree` -- Hue Shift +* `lshift:percentage` -- Lightness Shift +* `sshift:percentage` -- Saturation Shift +* `rshift:magnitude` -- Redness Shift +* `gshift:magnitude` -- Greenness Shift +* `bshift:magnitude` -- Blueness Shift + +The output will be located at `./output/custom.bmp`. + +Running the cli without arguments will produce a sample of all transformations. diff --git a/assets/palette-bitmap.bmp b/bitmap-autobots/assets/palette-bitmap.bmp similarity index 100% rename from assets/palette-bitmap.bmp rename to bitmap-autobots/assets/palette-bitmap.bmp diff --git a/bitmap-autobots/index.js b/bitmap-autobots/index.js new file mode 100644 index 0000000..2cc0b5e --- /dev/null +++ b/bitmap-autobots/index.js @@ -0,0 +1,134 @@ +'use strict'; + +const fs = require('fs'); +const Bitmap = require('./lib/bitmap.js'); +require('./lib/bitmap-transforms.js'); + +let checkOutputDirectoryExists = function() { + return new Promise(function(resolve, reject) { + fs.exists('./output/', function(exists) { + if (exists) { + resolve(); + } + else { + reject(); + } + }); + }); +}; + +let createOutputDirectory = function() { + return new Promise(function(resolve, reject) { + fs.mkdir('./output', function(err) { + if (err) reject(err); + resolve(); + }); + }); +}; + +let loadBitmap = function(filename) { + return new Promise(function(resolve, reject) { + fs.readFile(filename, function(err, buffer) { + if (err) reject(err); + resolve(new Bitmap(buffer)); + }); + }); +}; + +let createOutput = function(fileName, bitmap, func) { + return new Promise(function(resolve, reject) { + let transformedBitmap = func(); + + fs.writeFile(`./output/${fileName}.bmp`, transformedBitmap.buffer, 'binary', function(err) { + if (err) reject(err); + resolve(bitmap); + }); + }); +}; + +let filePath = process.argv[2]; +let transforms = process.argv.slice(3); + +if (filePath && transforms) { + checkOutputDirectoryExists() + .catch(createOutputDirectory) + .then(() => loadBitmap(filePath)) + .then(bitmap => { + while (transforms.length > 0) { + let transform = transforms.shift(); + let transformParts = transform.split(':'); + + switch (transformParts[0]) { + case 'bw': + bitmap = bitmap.toBlackAndWhite(); + break; + case 'hflip': + bitmap = bitmap.flipHorizontally(); + break; + case 'vflip': + bitmap = bitmap.flipVertically(); + break; + case 'gray': + bitmap = bitmap.toGrayscale(); + break; + case 'invert': + bitmap = bitmap.invertColors(); + break; + case 'rotc': + bitmap = bitmap.rotateClockwise(); + break; + case 'rotcc': + bitmap = bitmap.rotateCounterclockwise(); + break; + case 'sepia': + bitmap = bitmap.toSepia(); + break; + case 'bshift': + bitmap = bitmap.shiftBlueness(Number(transformParts[1])); + break; + case 'gshift': + bitmap = bitmap.shiftGreenness(Number(transformParts[1])); + break; + case 'rshift': + bitmap = bitmap.shiftRedness(Number(transformParts[1])); + break; + case 'sshift': + bitmap = bitmap.shiftSaturation(Number(transformParts[1])); + break; + case 'hshift': + bitmap = bitmap.shiftHue(Number(transformParts[1])); + break; + case 'lshift': + bitmap = bitmap.shiftLightness(Number(transformParts[1])); + break; + default: + break; + } + } + + createOutput('custom', bitmap, () => bitmap); + + }) + .catch(err => console.error(err)); + + return; +} + + +checkOutputDirectoryExists() + .catch(createOutputDirectory) + .then(() => loadBitmap(`${__dirname}/assets/palette-bitmap.bmp`), console.error) + .then(bitmap => createOutput('black-and-white', bitmap, () => bitmap.toBlackAndWhite()), console.error) + .then(bitmap => createOutput('grayscale', bitmap, () => bitmap.toGrayscale()), console.error) + .then(bitmap => createOutput('sepia', bitmap, () => bitmap.toSepia()), console.error) + .then(bitmap => createOutput('inverted-colors', bitmap, () => bitmap.invertColors()), console.error) + .then(bitmap => createOutput('flipped-horizontally', bitmap, () => bitmap.flipHorizontally()), console.error) + .then(bitmap => createOutput('flipped-vertically', bitmap, () => bitmap.flipVertically()), console.error) + .then(bitmap => createOutput('rotate-clockwise', bitmap, () => bitmap.rotateClockwise()), console.error) + .then(bitmap => createOutput('rotate-counterclockwise', bitmap, () => bitmap.rotateCounterclockwise()), console.error) + .then(bitmap => createOutput('shift-hue', bitmap, () => bitmap.shiftHue(60)), console.error) + .then(bitmap => createOutput('shift-saturation', bitmap, () => bitmap.shiftSaturation(-30)), console.error) + .then(bitmap => createOutput('shift-lightness', bitmap, () => bitmap.shiftLightness(20)), console.error) + .then(bitmap => createOutput('shift-redness', bitmap, () => bitmap.shiftRedness(5)), console.error) + .then(bitmap => createOutput('shift-greenness', bitmap, () => bitmap.shiftGreenness(5)), console.error) + .then(bitmap => createOutput('shift-blueness', bitmap, () => bitmap.shiftBlueness(5)), console.error); \ No newline at end of file diff --git a/bitmap-autobots/lib/bitmap-transformer.js b/bitmap-autobots/lib/bitmap-transformer.js new file mode 100644 index 0000000..5f4681a --- /dev/null +++ b/bitmap-autobots/lib/bitmap-transformer.js @@ -0,0 +1,65 @@ +'use strict'; + +const Bitmap = require('./bitmap.js'); + +module.exports = BitmapTransformer; + +function BitmapTransformer(bitmap) { + if (!(bitmap instanceof Bitmap)) { + throw new TypeError('The input is not a bitmap.'); + } + + this.bitmap = bitmap; +} + +BitmapTransformer.prototype.addColor = function(color) { + let uniqueColors = {}; + + for (let i = 0; i < this.bitmap.colors.length; i++) { + let uniqueColor = uniqueColors[this.bitmap.colors[i].toRGBAString()]; + + if (uniqueColor) { + this.bitmap.colors[i] = color; + return i; + } + else { + uniqueColors[color.toRGBAString()] = this.bitmap.colors[i]; + } + } +}; + +BitmapTransformer.prototype.writeColors = function() { + let bgraColors = this.bitmap.colors.map(c => c.toBGRAString()).join(''); + this.bitmap.buffer.write(bgraColors, this.bitmap.colorTableOffset, this.bitmap.colorTableSize, 'hex'); +}; + +BitmapTransformer.prototype.writePixels = function() { + for (var y = 0; y < this.bitmap.height; y++) { + for (var x = 0; x < this.bitmap.width; x++) { + this.writePixel(x, y, this.bitmap.pixelArray[y][x]); + } + } +}; + +BitmapTransformer.prototype.writePixel = function(x, y, color) { + let pixelOffset = this.bitmap.pixelArrayOffset + (this.bitmap.height - 1 - y) * this.bitmap.pixelRowSize + x * this.bitmap.bytesPerPixel; + + let colorIndex = -1; + + for (var i = 0; i < this.bitmap.colors.length; i++) { + let matchesRed = this.bitmap.colors[i].red === color.red; + let matchesGreen = this.bitmap.colors[i].green === color.green; + let matchesBlue = this.bitmap.colors[i].blue === color.blue; + + if (matchesRed && matchesGreen && matchesBlue) { + colorIndex = i; + break; + } + } + + if (colorIndex === -1) { + colorIndex = this.addColor(color); + } + + this.bitmap.buffer.writeUInt8(colorIndex, pixelOffset); +}; \ No newline at end of file diff --git a/bitmap-autobots/lib/bitmap-transforms.js b/bitmap-autobots/lib/bitmap-transforms.js new file mode 100644 index 0000000..01b3bf8 --- /dev/null +++ b/bitmap-autobots/lib/bitmap-transforms.js @@ -0,0 +1,154 @@ +'use strict'; + +require('./color-transforms.js'); +const Bitmap = require('./bitmap.js'); +const BitmapTransformer = require('./bitmap-transformer.js'); + +Bitmap.prototype.invertColors = function() { + let clone = this.clone(); + let bitmapTransformer = new BitmapTransformer(clone); + + clone.colors = clone.colors.map(c => c.invertColors()); + + bitmapTransformer.writeColors(); + return clone; +}; + +Bitmap.prototype.toSepia = function() { + let clone = this.clone(); + let bitmapTransformer = new BitmapTransformer(clone); + + clone.colors = clone.colors.map(c => c.toSepia()); + + bitmapTransformer.writeColors(); + return clone; +}; + +Bitmap.prototype.toGrayscale = function() { + let clone = this.clone(); + let bitmapTransformer = new BitmapTransformer(clone); + + clone.colors = clone.colors.map(c => c.toGrayscale()); + + bitmapTransformer.writeColors(); + return clone; +}; + +Bitmap.prototype.toBlackAndWhite = function() { + let clone = this.clone(); + let bitmapTransformer = new BitmapTransformer(clone); + + clone.colors = clone.colors.map(c => c.toBlackAndWhite()); + + bitmapTransformer.writeColors(); + return clone; +}; + +Bitmap.prototype.flipHorizontally = function() { + let clone = this.clone(); + let bitmapTransformer = new BitmapTransformer(clone); + + for (var i = 0; i < clone.height; i++) { + clone.pixelArray[i] = clone.pixelArray[i].reverse(); + } + + bitmapTransformer.writePixels(); + return clone; +}; + +Bitmap.prototype.flipVertically = function() { + let clone = this.clone(); + let bitmapTransformer = new BitmapTransformer(clone); + + clone.pixelArray = clone.pixelArray.reverse(); + + bitmapTransformer.writePixels(); + return clone; +}; + +Bitmap.prototype.rotateClockwise = function() { + let clone = this.clone(); + let bitmapTransformer = new BitmapTransformer(clone); + + for (var y = 0; y < clone.height; y++) { + for (var x = 0; x < clone.width; x++) { + bitmapTransformer.writePixel(clone.height - 1 - y, x, clone.pixelArray[y][x]); + } + } + + return clone; +}; + +Bitmap.prototype.rotateCounterclockwise = function() { + let clone = this.clone(); + let bitmapTransformer = new BitmapTransformer(clone); + + for (var y = 0; y < clone.height; y++) { + for (var x = 0; x < clone.width; x++) { + bitmapTransformer.writePixel(y, clone.width - 1 - x, clone.pixelArray[y][x]); + } + } + + return clone; +}; + +Bitmap.prototype.shiftHue = function(degrees) { + let clone = this.clone(); + let bitmapTransformer = new BitmapTransformer(clone); + + clone.colors = clone.colors.map(c => c.shiftHue(degrees)); + + bitmapTransformer.writeColors(); + return clone; +}; + +Bitmap.prototype.shiftSaturation = function(percentage) { + let clone = this.clone(); + let bitmapTransformer = new BitmapTransformer(clone); + + clone.colors = clone.colors.map(c => c.shiftSaturation(percentage)); + + bitmapTransformer.writeColors(); + return clone; +}; + +Bitmap.prototype.shiftLightness = function(percentage) { + let clone = this.clone(); + let bitmapTransformer = new BitmapTransformer(clone); + + clone.colors = clone.colors.map(c => c.shiftLightness(percentage)); + + bitmapTransformer.writeColors(); + return clone; +}; + +Bitmap.prototype.shiftRedness = function(magnitude) { + let clone = this.clone(); + let bitmapTransformer = new BitmapTransformer(clone); + + clone.colors = clone.colors.map(c => c.shiftRedness(magnitude)); + + bitmapTransformer.writeColors(); + return clone; +}; + + +Bitmap.prototype.shiftGreenness = function(magnitude) { + let clone = this.clone(); + let bitmapTransformer = new BitmapTransformer(clone); + + clone.colors = clone.colors.map(c => c.shiftGreenness(magnitude)); + + bitmapTransformer.writeColors(); + return clone; +}; + +Bitmap.prototype.shiftBlueness = function(magnitude) { + let clone = this.clone(); + let bitmapTransformer = new BitmapTransformer(clone); + + clone.colors = clone.colors.map(c => c.shiftBlueness(magnitude)); + + bitmapTransformer.writeColors(); + return clone; +}; \ No newline at end of file diff --git a/bitmap-autobots/lib/bitmap.js b/bitmap-autobots/lib/bitmap.js new file mode 100644 index 0000000..86e2a1a --- /dev/null +++ b/bitmap-autobots/lib/bitmap.js @@ -0,0 +1,157 @@ +'use strict'; + +require('./buffer-extensions.js'); +const Color = require('./color.js'); + +module.exports = Bitmap; + +function Bitmap(buffer) { + if (!buffer) throw new ReferenceError('buffer cannot be undefined.'); + + this.buffer = buffer; + + readHeader.call(this); + readBitmapHeader.call(this); + readColorTable.call(this); + readPixelArray.call(this); +} + +Bitmap.prototype.clone = function() { + return new Bitmap(Buffer.from(this.buffer)); +}; + +function readHeader() { + this.type = this.buffer.toString('utf-8', 0, 2); + this.size = this.buffer.readInt32(2); + this.reserved1 = this.buffer.readInt32(6); + this.reserved2 = this.buffer.readInt32(8); + this.pixelArrayOffset = this.buffer.readInt32(10); +} + +function readBitmapHeader() { + this.bitmapHeaderSize = this.buffer.readUInt32(14); + + if (this.bitmapHeaderSize === 12) { + readBitmapCoreHeader.call(this); + return; + } + + this.colorPlanes = this.buffer.readUInt16(26); + this.bitsPerPixel = this.buffer.readUInt16(28); + this.compression = this.buffer.readUInt32(30); + this.size = this.buffer.readUInt32(34); + this.colorCount = this.buffer.readUInt32(46); + this.importantColorCount = this.buffer.readUInt32(50); + + if (this.bitmapHeaderSize === 16 || this.bitmapHeaderSize === 64) { + readOS22BitmapHeader.call(this); + return; + } + + this.width = this.buffer.readInt32(18); + this.height = this.buffer.readInt32(22); + this.horizontalResolution = this.buffer.readInt32(38); + this.verticalResolution = this.buffer.readInt32(42); + + if (this.bitmapHeaderSize === 40) { + return; + } + + this.redMask = this.buffer.readUInt32(54); + this.greenMask = this.buffer.readUInt32(58); + this.blueMask = this.buffer.readUInt32(62); + this.alphaMask = this.buffer.readUInt32(66); + this.colorSpaceType = this.buffer.readUInt32(70); + this.endpoints = getCIEXYZTriple(this.buffer.slice(74, 110)); + this.gammaRed = this.buffer.readUInt32(110); + this.gammaGreen = this.buffer.readUInt32(114); + this.gammaBlue = this.buffer.readUInt32(118); + this.intent = this.buffer.readUInt32(122); + this.profileData = this.buffer.readUInt32(126); + this.profileSize = this.buffer.readUInt32(130); + this.reserved = this.buffer.readUInt32(134); +} + +function readBitmapCoreHeader() { + this.width = this.buffer.readUInt16(18); + this.height = this.buffer.readUInt16(20); + this.colorPlanes = this.buffer.readUInt16(22); + this.bitsPerPixel = this.buffer.readUInt16(24); +} + +function readOS22BitmapHeader() { + this.width = this.buffer.readUInt32(18); + this.height = this.buffer.readUInt32(22); + this.horizontalResolution = this.buffer.readUInt32(38); + this.verticalResolution = this.buffer.readUInt32(42); + + let short = this.bitmapHeaderSize == 16; + + this.resolutionUnit = short ? 0 : this.buffer.readUInt16(54); + this.reserved = short ? 0 : this.buffer.readUInt16(56); + this.orientation = short ? 0 : this.buffer.readUInt16(58); + this.halftoning = short ? 0 : this.buffer.readUInt16(60); + this.halftoneSize1 = short ? 0 : this.buffer.readUInt32(62); + this.halftoneSize2 = short ? 0 : this.buffer.readUInt32(66); + this.colorSpace = short ? 0 : this.buffer.readUInt32(70); + this.appData = short ? 0 : this.buffer.readUInt32(74); +} + +function getCIEXYZTriple(buffer) { + let red = new CIEXYZ(buffer.readFloat(0), buffer.readFloat(4), buffer.readFloat(8)); + let green = new CIEXYZ(buffer.readFloat(12), buffer.readFloat(16), buffer.readFloat(20)); + let blue = new CIEXYZ(buffer.readFloat(24), buffer.readFloat(28), buffer.readFloat(32)); + + return new CIEXYZTriple(red, green, blue); +} + +function CIEXYZ(x, y, z) { + this.x = x; + this.y = y; + this.z = z; +} + +function CIEXYZTriple(red, green, blue) { + this.red = red; + this.green = green; + this.blue = blue; +} + +function readColorTable() { + this.colors = []; + this.colorTableOffset = 14 + this.bitmapHeaderSize; + this.colorTableSize = this.colorCount * 4; + + for (let i = this.colorTableOffset; i < this.colorTableOffset + this.colorTableSize; i += 4) { + let bgraHex = this.buffer.toString('hex', i, i + 4); + let color = Color.fromBGRAString(bgraHex); + this.colors.push(color); + } +} + +function readPixelArray() { + this.pixelRowSize = Math.ceil(this.bitsPerPixel * this.width / 32) * 4; + this.bytesPerPixel = this.bitsPerPixel / 8; + + this.pixelArray = []; + + for (let row = this.height - 1; row >= 0; row--) { + let pixelRow = []; + let rowOffset = this.pixelArrayOffset + row * this.pixelRowSize; + + for (let pixel = 0; pixel < this.width; pixel++) { + let pixelOffset = rowOffset + pixel * this.bytesPerPixel; + + if (this.bitsPerPixel < 16) { + let colorIndex = this.buffer.readUInt8(pixelOffset); + pixelRow.push(this.colors[colorIndex]); + } else { + let bgraHex = this.buffer.toString('hex', pixelOffset, this.bytesPerPixel); + let color = Color.fromBGRAString(bgraHex); + pixelRow.push(color); + } + } + + this.pixelArray.push(pixelRow); + } +} \ No newline at end of file diff --git a/bitmap-autobots/lib/buffer-extensions.js b/bitmap-autobots/lib/buffer-extensions.js new file mode 100644 index 0000000..5eccd9b --- /dev/null +++ b/bitmap-autobots/lib/buffer-extensions.js @@ -0,0 +1,49 @@ +'use strict'; + +const os = require('os'); + +let endianness = os.endianness(); + +Buffer.prototype.read = function(offset, valueType) { + if (endianness === 'LE') { + switch (valueType) { + case 'int32': + return this.readInt32LE(offset); + case 'uint16': + return this.readUInt16LE(offset); + case 'uint32': + return this.readInt32LE(offset); + case 'float': + return this.readFloatLE(offset); + } + } else { + switch (valueType) { + case 'int32': + return this.readInt32BE(offset); + case 'uint16': + return this.readUInt16BE(offset); + case 'uint32': + return this.readInt32BE(offset); + case 'float': + return this.readFloatBE(offset); + } + } + + return null; +}; + +Buffer.prototype.readInt32 = function(offset) { + return this.read(offset, 'int32'); +}; + +Buffer.prototype.readUInt16 = function(offset) { + return this.read(offset, 'uint16'); +}; + +Buffer.prototype.readUInt32 = function(offset) { + return this.read(offset, 'uint32'); +}; + +Buffer.prototype.readFloat = function(offset) { + return this.read(offset, 'float'); +}; \ No newline at end of file diff --git a/bitmap-autobots/lib/color-transforms.js b/bitmap-autobots/lib/color-transforms.js new file mode 100644 index 0000000..a7d6a72 --- /dev/null +++ b/bitmap-autobots/lib/color-transforms.js @@ -0,0 +1,64 @@ +'use strict'; + +const Color = require('./color'); + +Color.prototype.invertColors = function() { + return new Color(255 - this.red, 255 - this.green, 255 - this.blue, this.alpha); +}; + +Color.prototype.toSepia = function() { + let red = 0.393 * this.red + 0.769 * this.green + 0.189 * this.blue; + let green = 0.349 * this.red + 0.686 * this.green + 0.168 * this.blue; + let blue = 0.272 * this.red + 0.534 * this.green + 0.131 * this.blue; + return new Color(Math.min(Math.round(red), 255), Math.min(Math.round(green), 255), Math.min(Math.round(blue), 255)); +}; + +Color.prototype.toGrayscale = function() { + let average = Math.round((this.red + this.green + this.blue) / 3); + return new Color(average, average, average, this.alpha); +}; + +Color.prototype.toBlackAndWhite = function() { + let average = Math.round((this.red + this.green + this.blue) / 3); + let result = average < 128 ? 0 : 255; + return new Color(result, result, result, this.alpha); +}; + +Color.prototype.shiftRedness = function(magnitude) { + return new Color(Math.max(0, Math.min(255, this.red * magnitude)), this.green, this.blue, this.alpha); +}; + +Color.prototype.shiftGreenness = function(magnitude) { + return new Color(this.red, Math.max(0, Math.min(255, this.green * magnitude)), this.blue, this.alpha); +}; + +Color.prototype.shiftBlueness = function(magnitude) { + return new Color(this.red, this.green, Math.max(0, Math.min(255, this.blue * magnitude)), this.alpha); +}; + +Color.prototype.shiftHue = function(degrees) { + this.getHSL(); + let hue = this.hue + degrees; + + if (Math.abs(hue) > 360) { + hue *= (1 / (hue / 360)); + } + + if (hue < 0) { + hue += 360; + } + + return Color.fromHSLA(hue, this.saturation, this.lightness, this.alpha); +}; + +Color.prototype.shiftSaturation = function(percentage) { + this.getHSL(); + let saturation = Math.min(1, Math.max(0, this.saturation + percentage / 100)); + return Color.fromHSLA(this.hue, saturation, this.lightness, this.alpha); +}; + +Color.prototype.shiftLightness = function(percentage) { + this.getHSL(); + let lightness = Math.min(1, Math.max(0, this.lightness + percentage / 100)); + return Color.fromHSLA(this.hue, this.saturation, lightness, this.alpha); +}; \ No newline at end of file diff --git a/bitmap-autobots/lib/color.js b/bitmap-autobots/lib/color.js new file mode 100644 index 0000000..fa237cc --- /dev/null +++ b/bitmap-autobots/lib/color.js @@ -0,0 +1,131 @@ +'use strict'; + +module.exports = Color; + +function Color(red, green, blue, alpha = 0) { + if (typeof red !== 'number') throw new TypeError('red was not a number'); + if (typeof green !== 'number') throw new TypeError('green was not a number'); + if (typeof blue !== 'number') throw new TypeError('blue was not a number'); + + if (red < 0 || red > 255) throw new RangeError('red was out of range'); + if (green < 0 || green > 255) throw new RangeError('green was out of range'); + if (blue < 0 || blue > 255) throw new RangeError('blue was out of range'); + + this.red = red; + this.green = green; + this.blue = blue; + this.alpha = alpha; +} + +Color.prototype.getHSL = function() { + let redness = this.red / 255; + let greenness = this.green / 255; + let blueness = this.blue / 255; + + let min = Math.min(redness, greenness, blueness); + let max = Math.max(redness, greenness, blueness); + let difference = max - min; + + this.hue = difference; + this.saturation = difference; + this.lightness = (min + max) / 2; + + if (difference === 0) { + this.saturation = 0; + this.hue = 0; + } + else { + this.saturation = difference / (1 - Math.abs(2 * this.lightness - 1)); + + if (max === redness) { + this.hue = Math.round(60 * (((greenness - blueness) / difference) % 6)); + } else if (max === greenness) { + this.hue = Math.round(60 * (((blueness - redness) / difference) + 2)); + } else if (max === blueness) { + this.hue = Math.round(60 * (((redness - greenness) / difference) + 4)); + } + + if (this.hue < 0) { + this.hue += 360; + } + } +}; + +Color.prototype.toBGRAString = function() { + return toPaddedHex(this.blue) + toPaddedHex(this.green) + toPaddedHex(this.red) + toPaddedHex(this.alpha); +}; + +Color.prototype.toRGBAString = function() { + return toPaddedHex(this.red) + toPaddedHex(this.green) + toPaddedHex(this.blue) + toPaddedHex(this.alpha); +}; + +function toPaddedHex(channel) { + channel = channel.toString(16); + + if (channel.length === 1) { + return '0' + channel; + } + + return channel; +} + +Color.fromRGBAString = function(rgbaHex) { + let red = parseInt(rgbaHex.slice(0, 2), 16); + let green = parseInt(rgbaHex.slice(2, 4), 16); + let blue = parseInt(rgbaHex.slice(4, 6), 16); + let alpha = parseInt(rgbaHex.slice(6, 8), 16); + + return new Color(red, green, blue, alpha); +}; + +Color.fromBGRAString = function(rgbaHex) { + let blue = parseInt(rgbaHex.slice(0, 2), 16); + let green = parseInt(rgbaHex.slice(2, 4), 16); + let red = parseInt(rgbaHex.slice(4, 6), 16); + let alpha = parseInt(rgbaHex.slice(6, 8), 16); + + return new Color(red, green, blue, alpha); +}; + +Color.fromHSLA = function(hue, saturation, lightness, alpha = 0) { + let chroma = (1 - Math.abs(2 * lightness - 1)) * saturation; + let h = hue / 60; + let x = chroma * (1 - Math.abs(h % 2 - 1)); + + let red = 0; + let green = 0; + let blue = 0; + + if (0 <= h && h <= 1) { + red = chroma; + green = x; + } else if (1 <= h && h <= 2) { + red = x; + green = chroma; + } else if (2 <= h && h <= 3) { + green = chroma; + blue = x; + } else if (3 <= h && h <= 4) { + green = x; + blue = chroma; + } else if (4 <= h && h <= 5) { + red = x; + blue = chroma; + } else if (5 <= h && h <= 6) { + red = chroma; + blue = x; + } + + let m = lightness - 0.5 * chroma; + + red = Math.round((red + m) * 255); + green = Math.round((green + m) * 255); + blue = Math.round((blue + m) * 255); + + let color = new Color(red, green, blue, alpha); + color.hue = hue; + color.saturation = saturation; + color.lightness = lightness; + + return color; +}; \ No newline at end of file diff --git a/bitmap-autobots/package.json b/bitmap-autobots/package.json new file mode 100644 index 0000000..6213820 --- /dev/null +++ b/bitmap-autobots/package.json @@ -0,0 +1,21 @@ +{ + "name": "bitmap-autobots", + "version": "1.0.0", + "description": "", + "main": "index.js", + "directories": { + "test": "test" + }, + "scripts": { + "start": "node index.js", + "test": "mocha", + "lint": "eslint ." + }, + "keywords": [], + "author": "", + "license": "MIT", + "devDependencies": { + "chai": "^4.1.0", + "mocha": "^3.4.2" + } +} diff --git a/bitmap-autobots/test/bitmap-test.js b/bitmap-autobots/test/bitmap-test.js new file mode 100644 index 0000000..9f86882 --- /dev/null +++ b/bitmap-autobots/test/bitmap-test.js @@ -0,0 +1,71 @@ +'use strict'; + +const fs = require('fs'); +const expect = require('chai').expect; +const Bitmap = require('./../lib/bitmap.js'); + +var bitmap; + +describe('Bitmap', function() { + before(done => { + fs.readFile(`${__dirname}/../assets/palette-bitmap.bmp`, function(err, buffer) { + if (err) throw err; + bitmap = new Bitmap(buffer); + done(); + }); + }); + + describe('#()', () => { + it('should throw error if no buffer provided.', () => { + expect(() => new Bitmap()).to.throw(ReferenceError); + }); + }); + + describe('#readHeader()', () => { + it('should return the correct property values.', () => { + expect(bitmap).to.have.property('type', 'BM'); + expect(bitmap).to.have.property('size', 10000); + expect(bitmap).to.have.property('reserved1', 0); + expect(bitmap).to.have.property('reserved2', 70647808); + expect(bitmap).to.have.property('pixelArrayOffset', 1078); + }); + }); + + describe('#readBitmapHeader()', () => { + it('should return the correct property values.', () => { + expect(bitmap).to.have.property('bitmapHeaderSize', 40); + }); + }); + + describe('#readBitmapInfoHeader()', () => { + it('should return the correct property values.', () => { + expect(bitmap).to.have.property('width', 100); + expect(bitmap).to.have.property('height', 100); + expect(bitmap).to.have.property('colorPlanes', 1); + expect(bitmap).to.have.property('bitsPerPixel', 8); + expect(bitmap).to.have.property('compression', 0); + expect(bitmap).to.have.property('horizontalResolution', 2834); + expect(bitmap).to.have.property('verticalResolution', 2834); + expect(bitmap).to.have.property('colorCount', 256); + expect(bitmap).to.have.property('importantColorCount', 256); + }); + }); + + describe('#readColorTable()', () => { + it('should return the correct property values.', () => { + expect(bitmap).to.have.property('colors'); + expect(bitmap.colors.length).to.equal(256); + expect(bitmap).to.have.property('colorTableOffset', 54); + expect(bitmap).to.have.property('colorTableSize', 1024); + }); + }); + + describe('#readPixelArray()', () => { + it('should return the correct property values.', () => { + expect(bitmap).to.have.property('pixelRowSize', 100); + expect(bitmap).to.have.property('bytesPerPixel', 1); + expect(bitmap).to.have.property('pixelArray'); + expect(bitmap.pixelArray.length).to.equal(100); + }); + }); +}); diff --git a/bitmap-autobots/test/bitmap-transformer-test.js b/bitmap-autobots/test/bitmap-transformer-test.js new file mode 100644 index 0000000..77cb400 --- /dev/null +++ b/bitmap-autobots/test/bitmap-transformer-test.js @@ -0,0 +1,12 @@ +'use strict'; + +const expect = require('chai').expect; +const BitmapTransformer = require('./../lib/bitmap-transformer.js'); + +describe('Bitmap Transformer', function() { + describe('#()', () => { + it('should throw errors if the input is not a bitmap.', () => { + expect(() => new BitmapTransformer()).to.throw(TypeError); + }); + }); +}); diff --git a/bitmap-autobots/test/color-test.js b/bitmap-autobots/test/color-test.js new file mode 100644 index 0000000..6c5936d --- /dev/null +++ b/bitmap-autobots/test/color-test.js @@ -0,0 +1,86 @@ +'use strict'; + +const expect = require('chai').expect; +const Color = require('./../lib/color.js'); + +var color = new Color(10, 20, 30, 40); + +describe('Color', function() { + describe('#()', () => { + it('should throw errors if the input values are not numbers.', () => { + expect(() => new Color(0, 0)).to.throw(TypeError); + expect(() => new Color(0)).to.throw(TypeError); + expect(() => new Color()).to.throw(TypeError); + }); + + it('should throw errors if the input values are out of range.', () => { + expect(() => new Color(256, 0, 0)).to.throw(RangeError); + expect(() => new Color(0, 256, 0)).to.throw(RangeError); + expect(() => new Color(0, 0, 256)).to.throw(RangeError); + }); + + it('should return the correct property values.', () => { + expect(color).to.have.property('red', 10); + expect(color).to.have.property('green', 20); + expect(color).to.have.property('blue', 30); + expect(color).to.have.property('alpha', 40); + }); + }); + + describe('#getHSL()', () => { + it('should return the correct property values.', () => { + color.getHSL(); + expect(color).to.have.property('hue', 210); + expect(color).to.have.property('saturation', 0.5); + expect(color).to.have.property('lightness', 0.0784313725490196); + expect(color).to.have.property('alpha', 40); + }); + }); + + describe('#toBGRAString()', () => { + it('should return the correct hex value.', () => { + let bgraString = color.toBGRAString(); + expect(bgraString).to.equal('1e140a28'); + }); + }); + + describe('#toRGBAString()', () => { + it('should return the correct hex value.', () => { + let rgbaString = color.toRGBAString(); + expect(rgbaString).to.equal('0a141e28'); + }); + }); + + describe('#fromRGBAString()', () => { + it('should return the correct property values.', () => { + let rgba = Color.fromRGBAString('0a141e28'); + expect(rgba.red).to.equal(10); + expect(rgba.green).to.equal(20); + expect(rgba.blue).to.equal(30); + expect(rgba.alpha).to.equal(40); + }); + }); + + describe('#fromBGRAString()', () => { + it('should return the correct property values.', () => { + let bgra = Color.fromBGRAString('1e140a28'); + expect(bgra.red).to.equal(10); + expect(bgra.green).to.equal(20); + expect(bgra.blue).to.equal(30); + expect(bgra.alpha).to.equal(40); + }); + }); + + describe('#fromHSLA()', () => { + it('should return the correct property values.', () => { + let hsla = Color.fromHSLA(210, 0.5, 0.0784313725490196, 40); + expect(hsla.red).to.equal(10); + expect(hsla.green).to.equal(20); + expect(hsla.blue).to.equal(30); + expect(hsla.alpha).to.equal(40); + expect(hsla.hue).to.equal(210); + expect(hsla.saturation).to.equal(0.5); + expect(hsla.lightness).to.equal(0.0784313725490196); + }); + }); +}); \ No newline at end of file diff --git a/bitmap-autobots/test/color-transforms-test.js b/bitmap-autobots/test/color-transforms-test.js new file mode 100644 index 0000000..3091452 --- /dev/null +++ b/bitmap-autobots/test/color-transforms-test.js @@ -0,0 +1,36 @@ +'use strict'; + +const expect = require('chai').expect; +const Color = require('./../lib/color.js'); +require('./../lib/color-transforms.js'); + +var color = new Color(10, 20, 30, 40); + +describe('Color', function() { + describe('#toGreyscale()', () => { + it('should return the correct property values.', () => { + let grayscale = color.toGrayscale(); + expect(grayscale.red).to.equal(20); + expect(grayscale.green).to.equal(20); + expect(grayscale.blue).to.equal(20); + }); + }); + + describe('#invertColors()', () => { + it('should return the correct property values.', () => { + let inverse = color.invertColors(); + expect(inverse.red).to.equal(245); + expect(inverse.green).to.equal(235); + expect(inverse.blue).to.equal(225); + }); + }); + + describe('#toBlackAndWhite()', () => { + it('should return the correct property values.', () => { + let blackAndWhite = color.toBlackAndWhite(); + expect(blackAndWhite.red).to.equal(0); + expect(blackAndWhite.green).to.equal(0); + expect(blackAndWhite.blue).to.equal(0); + }); + }); +}); \ No newline at end of file