├── .github ├── FUNDING.yml └── workflows │ └── node.js.yml ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── index.js ├── lib ├── modes │ ├── abna.js │ ├── blurbobb.js │ ├── castles.js │ ├── chimera.js │ ├── fatcat.js │ ├── gazette.js │ ├── manticore95.js │ ├── schifty.js │ ├── template │ ├── vana.js │ ├── vaporwave.js │ ├── veneneux.js │ ├── void.js │ └── walter.js └── mosh.js ├── package-lock.json ├── package.json └── tests ├── fixtures ├── a.txt └── rgb.png └── preflight.test.js /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [mster] 4 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ "master" ] 9 | pull_request: 10 | branches: [ "master" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x, 16.x, 18.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'npm' 29 | - run: npm ci 30 | - run: npm test 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules/ 4 | example.js 5 | 6 | .env 7 | .env.test -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 14 4 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Jump in the pit and add something new! 4 | 5 | ## Adding a Mode 6 | 7 | By adding a new mode, you will be creating a new file which exports a function that manipulates data extracted from a source image's bitmap. That may sound like a lot, but it'll essentially come down to basic iteration over a buffer. Starter code has been provided [here](https://github.com/mster/datamosh/blob/master/lib/modes/template). 8 | 9 | ### Experimenting 10 | 11 | Your mosh mode doesn't have to do anything specific -- if you think it's cool, we probably will too. Use your imagination and feel free to experiment! 12 | 13 | Most of Datamosh's current modes overwrite existing Red/Green/Blue (RGB) pixel values with something random. If you get stuck or just need some inspiration, check out these examples. 14 | 15 | - [Vana mode](https://github.com/mster/datamosh/blob/master/lib/modes/vana.js) 16 | - [Schifty mode](https://github.com/mster/datamosh/blob/master/lib/modes/schifty.js) 17 | - [Before and after images](https://github.com/mster/datamosh#example-images) 18 | 19 | Once you have a stub function and some cool name for it, you'll want to import it into Datamosh. 20 | 21 | To import your mode, you have two options: 22 | 23 | 1. Import it into Datamosh by adding an entry in `mosh.MODES`, [here](https://github.com/mster/datamosh/blob/master/lib/mosh.js#L99). (Preferred) 24 | 2. Add it to the function property after you initialize Datamosh. Example code on how to do this can be found [here](https://github.com/mster/datamosh/releases/tag/v1.1.0). 25 | 26 | ### Getting Started 27 | 28 | 1. Fork datamosh and run `npm install` within the directory. 29 | 2. On your fork, create a new branch named `username/work-description`, where `username` is your GitHub username and `work-description` is a short description for your contribution. 30 | 31 | For example: `mster/new-mode` 32 | 33 | 3. Commit your work to the branch you created. 34 | 4. When you're ready for review or to submit your contribution, double check a few things. 35 | 36 | - ✓ First, make sure your fork is up to date. If your fork is out of date, you will need to rebase. 37 | 38 | - ✓ Next, assure that your code pases `npm test`. 39 | 40 | 5. If your contribution is more than a single commit, squash all commits into one. 41 | 6. Open a pull request to `mster:master`. 42 | 43 | ## Other Features 44 | 45 | Have an feature in mind? Go for it -- if we like it, we will add it. 46 | 47 | Follow the [Getting Started](#getting-started) guide if you're new to this. 48 | 49 | ## Code Style 50 | 51 | Datamosh uses [Prettier](https://prettier.io/). To have your contributions accepted, they must also be in StandardJS style. 52 | 53 | To test if your code passes, run the test command: 54 | `npm run lint` 55 | 56 | ## Testing 57 | 58 | As of v1.0.0, tests consist of: 59 | 60 | - linting using Prettier 61 | 62 | ## Need Help 63 | 64 | We're happy to help out, no matter how small. Open an issue, ping the author (@mster), or join the Discord. 65 | 66 | Datamosh has a dedicated Discord server. Feel free to join: https://discord.gg/DhYTmMD 67 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2020 Michael Sterpka 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Datamosh [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](https://opensource.org/licenses/MIT) [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) [![Build Status](https://travis-ci.com/mster/datamosh.svg?branch=master)](https://travis-ci.com/mster/datamosh) 2 | 3 | ![datamosh_cover_2x](https://user-images.githubusercontent.com/15038724/122327314-838e3d80-cee2-11eb-89d8-e315556797ba.png) 4 | 5 | Mess around with image data using _buffers_, create some interesting & artistic results, **profit**. 6 | 7 | # Install 8 | 9 | ``` 10 | $ npm install datamosh 11 | ``` 12 | 13 | # Usage 14 | 15 | ```js 16 | const mosh = require("datamosh"); 17 | 18 | let imgBuff = await readFile("/full/path/to/image.png"); 19 | 20 | let moshedBuff = await mosh(imgBuff, "vaporwave"); 21 | ``` 22 | 23 | Reading/Writing the moshed image 24 | 25 | ```js 26 | mosh("~/image.png", null, "~/moshed_image.png"); 27 | 28 | // because mode is null, a random mode will be chosen 29 | ``` 30 | 31 | Moshing a buffer with callbacks 32 | 33 | ```js 34 | const cb = (err, data) => { 35 | if (!err) writeFile("/path/to/out.gif", data); 36 | }; 37 | 38 | mosh(imgBuff, "vana", cb); 39 | ``` 40 | 41 | Using multiple modes on a single image, applied with respect to order. 42 | 43 | ```js 44 | let moshedBuff = await mosh(imgBuff, ["fatcat", "vaporwave", "walter"]); 45 | 46 | // ['vana', null, null] is also valid => ['vana', random, random] 47 | ``` 48 | 49 | # API 50 | 51 | ### `mosh(source, mode?, cb|writePath?)` 52 | 53 | Takes input `source` Buffer/Path, returns an encoded Buffer with the applied modes. 54 | 55 | - `mode`, the mosh mode to apply to the source image. Multiple modes may be passed using an array of modes. Any `null` values are replaced with a random mode. 56 | - `cb (err, data)`, when using callbacks. 57 | - `writePath`, the path to write the moshed image to. 58 | 59 | Paths may use the tilde (~) character. Datamosh validates read and write paths, replacing tilde with the path to the home directory. 60 | 61 | ``` 62 | ~/Desktop/moshes/ -> /home/youruser/Desktop/moshes 63 | ``` 64 | 65 | # Custom Modes 66 | 67 | Datamosh allows you to set custom moshing modes. As of `v1.1.0`, this may be acomplished by adding a mosh function to the `MODES` property. 68 | 69 | For mosh function starter code, see the included template file located [here](https://github.com/mster/datamosh/blob/master/lib/modes/template). 70 | 71 | ```js 72 | const datamosh = require("datamosh"); 73 | 74 | function newMode(data, width, height) { 75 | // your cool code goes here! 76 | 77 | return data; 78 | } 79 | 80 | datamosh.MODES.newMode = newMode; 81 | ``` 82 | 83 | ## Example Images 84 | 85 | ![mode:fatcat](https://user-images.githubusercontent.com/15038724/118332090-64088d00-b4be-11eb-8d6e-139174c5d3ab.png) 86 | Fatcat was created by user [@mster](https://github.com/mster) 87 | 88 | ![mode:vaporwave](https://user-images.githubusercontent.com/15038724/118332426-e8f3a680-b4be-11eb-9623-73be0128cc0a.png) 89 | Vaporwave was created by user [@tlaskey](https://github.com/tlaskey) 90 | 91 | ![mode:blurbobb](https://user-images.githubusercontent.com/15038724/118333046-dfb70980-b4bf-11eb-9142-97b91bbb6721.png) 92 | 93 | ![mode:veneneux](https://user-images.githubusercontent.com/15038724/118332676-4ee02e00-b4bf-11eb-9a71-9974933ad014.png) 94 | 95 | ## Datamosh in the wild 96 | 97 | Check out this list of awesome apps that use `datamosh`! 98 | 99 | - [JanMichaelBot](https://github.com/tlaskey/JanMichaelBot) by user [@Tlaskey](https://github.com/tlaskey) 100 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require("./lib/mosh"); 2 | -------------------------------------------------------------------------------- /lib/modes/abna.js: -------------------------------------------------------------------------------- 1 | const normalizeAlpha = x => x - (x % 4) + 3 2 | const randInt = (min, max) => (min + Math.random() * (max - min)) | 0 3 | const abna = (a, b) => (a << b) | (b << a) 4 | 5 | module.exports = function (data) { 6 | const ret = [] 7 | const sqLen = (Math.sqrt(data.length) * (5 + Math.random() * 15)) | 0 8 | const firstInflexion = normalizeAlpha(randInt(sqLen, data.length / 10)) 9 | 10 | for (let i = 0; i < data.length - 1; i++) { 11 | const offset = i % 4 12 | if (i === firstInflexion) ret.reverse() 13 | 14 | if (i % sqLen === 0) i += 4 15 | if (offset === 3) ret.push(data[i]) 16 | else ret.push(abna(data[i], offset === 2 ? data[i + 2] : data[i + 1])) 17 | } 18 | 19 | return ret 20 | } -------------------------------------------------------------------------------- /lib/modes/blurbobb.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (data) { 4 | let counter = 0; 5 | for (let i = 0; i < data.length; i++) { 6 | if (counter < 64) data[i] = Math.random() * 255; 7 | 8 | counter++; 9 | if (counter > 128) counter = Math.random() * 128; 10 | } 11 | 12 | return data; 13 | }; 14 | -------------------------------------------------------------------------------- /lib/modes/castles.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (data) { 4 | const red = new Array(data.length / 4), 5 | green = new Array(data.length / 4), 6 | blue = new Array(data.length / 4), 7 | a = new Array(data.length / 4); 8 | 9 | let high = 165, 10 | low = 80; 11 | for (let i = 0; i < data.length / 4; i++) { 12 | if (data[i] < high && data[i] > low) red[i] = data[i]; 13 | if (data[i + 1] < high && data[i + 1] > low) green[i] = data[i + 1]; 14 | if (data[i + 2] < high && data[i + 2] > low) blue[i] = data[i + 2]; 15 | a[i] = data[i + 3]; 16 | } 17 | 18 | const ret = []; 19 | for (let i = 0; i < red.length; i++) { 20 | ret.push(red[i]); 21 | ret.push(green[i]); 22 | ret.push(blue[i]); 23 | ret.push(a[i]); 24 | } 25 | 26 | return ret; 27 | }; 28 | -------------------------------------------------------------------------------- /lib/modes/chimera.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** Configurable variables */ 4 | const noiseThreshold = 0.2; 5 | const grainThreshold = 0.4; 6 | const chimeraWeight = [0.25, 0.5]; 7 | 8 | /** Math references */ 9 | const min = Math.min, 10 | max = Math.max, 11 | floor = Math.floor, 12 | random = Math.random; 13 | 14 | /** Randomizer */ 15 | function r(min, max) { 16 | return Math.floor(Math.random() * (max - min + 1) + min); 17 | } 18 | 19 | /** Chimera effect */ 20 | function chimera(data, width, height, weight = [0.25, 0.5]) { 21 | for (let y = 0; y < height; y++) { 22 | for (let x = 0; x < width; x++) { 23 | let index = (y * width + x) * 4; 24 | 25 | let r = data[index], 26 | g = data[index + 1], 27 | b = data[index + 2]; 28 | 29 | /** Apply different weights to each color channel */ 30 | data[index] = r + g * weight[1] + b * weight[0]; 31 | data[index + 1] = r * weight[1] + g + b * weight[0]; 32 | data[index + 2] = r * weight[0] + g * weight[1] + b; 33 | } 34 | } 35 | 36 | return data; 37 | } 38 | 39 | /** Blurs a buffer */ 40 | function blur(data, width, height, blurDirection, intensity = 5) { 41 | /** Create copy of buffer */ 42 | const buffer = Buffer.alloc(data.length); 43 | data.copy(buffer); 44 | 45 | /** Iterate through the image, averaging pixels in the specified direction */ 46 | for (let y = 0; y < height; y++) { 47 | for (let x = 0; x < width; x++) { 48 | let index = (y * width + x) * 4; 49 | 50 | let sumR = 0, 51 | sumG = 0, 52 | sumB = 0, 53 | count = 0; 54 | 55 | for (let i = 1; i <= intensity; i++) { 56 | if (blurDirection === "horizontal") { 57 | if (x + i < width) { 58 | let nextIndex = (y * width + x + i) * 4; 59 | 60 | sumR += data[nextIndex]; 61 | sumG += data[nextIndex + 1]; 62 | sumB += data[nextIndex + 2]; 63 | 64 | count++; 65 | } 66 | } else if (blurDirection === "vertical") { 67 | if (y + i < height) { 68 | let nextIndex = ((y + i) * width + x) * 4; 69 | sumR += data[nextIndex]; 70 | sumG += data[nextIndex + 1]; 71 | sumB += data[nextIndex + 2]; 72 | count++; 73 | } 74 | } 75 | } 76 | 77 | buffer[index] = sumR / count; 78 | buffer[index + 1] = sumG / count; 79 | buffer[index + 2] = sumB / count; 80 | } 81 | } 82 | 83 | return buffer; 84 | } 85 | 86 | /** Creates a "glitch" effect with randomly displaced rectangles */ 87 | function displaceRectangles(data, width, height) { 88 | /** Iterate through the image, selecting random rectangles */ 89 | for (let i = 0; i < 100; i++) { 90 | /** Choose rectangle dimensions */ 91 | const rectWidth = floor(random() * (width / 15)); 92 | const rectHeight = floor(random() * (height / 30)); 93 | 94 | /** Choose rectangle positions */ 95 | const rectX = floor(random() * (width - rectWidth)); 96 | const rectY = floor(random() * (height - rectHeight)); 97 | 98 | /** Displace the pixels within the rectangle */ 99 | let shift = floor(random() * (rectWidth / 4)); 100 | let shiftX = random() >= 0.5; 101 | 102 | const rMode = random(); 103 | 104 | /** Rectangle modifier */ 105 | const mode = rMode >= 0.66 ? r(5, 20) : rMode >= 0.33 ? null : -255; 106 | 107 | for (let y = rectY; y < rectY + rectHeight; y++) { 108 | for (let x = rectX; x < rectX + rectWidth; x++) { 109 | let pixelIndex = (y * width + x) * 4; 110 | 111 | /** Displace the pixel's index randomly */ 112 | let assignedIndex_X = (y * width + x + (shiftX ? shift : 0)) * 4; 113 | let assignedIndex_Y = ((y + (shiftX ? 0 : shift)) * width + x) * 4; 114 | 115 | for (let j = 0; j < 4; j++) { 116 | let temp = data[pixelIndex + j]; 117 | 118 | data[pixelIndex + j] = min( 119 | 255, 120 | data[assignedIndex_X + j] + (!mode ? -r(0, 20) : mode) 121 | ); 122 | 123 | data[assignedIndex_X + j] = min( 124 | 255, 125 | data[assignedIndex_Y + j] + (!mode ? -r(0, 20) : mode) 126 | ); 127 | 128 | data[assignedIndex_Y + j] = min( 129 | 255, 130 | temp + (!mode ? -r(0, 20) : mode) 131 | ); 132 | } 133 | } 134 | } 135 | } 136 | 137 | return data; 138 | } 139 | 140 | module.exports = function (data, width, height) { 141 | /** Choose blur direction and intensity */ 142 | const blurDirection = random() >= 0.5 ? "horizontal" : "vertical"; 143 | const blurIntensity = r(5, 10); 144 | 145 | /** Displace rectangles */ 146 | data = displaceRectangles(data, width, height); 147 | 148 | /** Blur image */ 149 | data = blur(data, width, height, blurDirection, blurIntensity); 150 | 151 | /** Apply `chimera` effect */ 152 | data = chimera(data, width, height, chimeraWeight); 153 | 154 | for (let index = 0; index < data.length; index += 4) { 155 | const useNoise = random() < noiseThreshold; 156 | const useGrain = random() < grainThreshold ? floor(random() * 50) : 0; 157 | 158 | [0, 1, 2].forEach((i) => { 159 | /** Add noise */ 160 | data[index + i] = useNoise 161 | ? min(data[index + i] + r(1, i === 0 ? 15 : 10), 255) 162 | : data[index + i]; 163 | 164 | /** Darken image */ 165 | data[index + i] = min( 166 | 255, 167 | max(0, data[index + i] + (floor(random() * 20) - 30)) 168 | ); 169 | 170 | /** Add grain */ 171 | data[index + i] = useGrain 172 | ? min(255, max(0, data[index + i] + useGrain)) 173 | : data[index + i]; 174 | }); 175 | } 176 | 177 | return data; 178 | }; 179 | -------------------------------------------------------------------------------- /lib/modes/fatcat.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (data) { 4 | const min = Math.min; 5 | 6 | // ??? 7 | for (let i = 0; i < data.length; i += 4) { 8 | data[i + 0] = min(data[i + 0] * 1.4, 280); 9 | data[i + 1] = min(data[i + 1] * 1.4, 280); 10 | data[i + 2] = min(data[i + 2] * 1.4, 280); 11 | } 12 | 13 | for (let i = 0; i < data.length; i += 4) { 14 | data[i + 0] = min(data[i + 0] * 1.4, 280); 15 | data[i + 1] = min(data[i + 1] * 1.4, 280); 16 | data[i + 2] = min(data[i + 2] * 1.4, 280); 17 | } 18 | 19 | for (let i = 0; i < data.length; i += 4) { 20 | data[i + 0] = min(data[i + 0] * 1.4, 256); 21 | data[i + 1] = min(data[i + 1] * 1.4, 256); 22 | data[i + 2] = min(data[i + 2] * 1.4, 256); 23 | } 24 | 25 | // ヾ(⌐■_■)ノ♪ 26 | for (let i = 0; i < data.length; i += 4) { 27 | data[i + 0] = min(data[i + 0] * 1.4, 255); 28 | data[i + 1] = min(data[i + 1] * 1.4, 255); 29 | data[i + 2] = min(data[i + 2] * 1.4, 255); 30 | } 31 | 32 | return data; 33 | }; 34 | -------------------------------------------------------------------------------- /lib/modes/gazette.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function (data) { 4 | const ret = [] 5 | 6 | for (let i = 0; i < data.length; i += 4) { 7 | if (i % 12 === 0) { 8 | ret.push(data[i]) 9 | ret.push(data[i + 1]) 10 | ret.push(data[i + 2]) 11 | ret.push(255) 12 | } else { 13 | const r = data[i] 14 | const g = data[i + 1] 15 | const b = data[i + 2] 16 | 17 | const max = Math.max(r, g, b) 18 | const min = Math.min(r, g, b) 19 | const L = (r + g + b) / 765 20 | 21 | let value = L > 0.65 ? 255 : L < 0.35 ? 0 : null 22 | 23 | for (let j = 0; j < 3; j++) { 24 | if (!value) ret.push(Math.random() > 0.5 ? max : min) 25 | else ret.push(value) 26 | } 27 | ret.push(255) 28 | } 29 | } 30 | 31 | return ret 32 | } 33 | -------------------------------------------------------------------------------- /lib/modes/manticore95.js: -------------------------------------------------------------------------------- 1 | const limiter = (x, min) => (x < min ? min : x) 2 | const getClosestRoot = x => x - (x % 4) 3 | const maxOffset = x => { 4 | let m = x[0] 5 | let o = 0 6 | for (let i = 1; i < x.length; i++) { 7 | if (x[i] > m) { 8 | o = i 9 | m = x[i] 10 | } 11 | } 12 | return [o, m] 13 | } 14 | 15 | module.exports = function (data, width) { 16 | const sqLen = Math.sqrt(data.length) / 8 17 | const ret = [] 18 | let i = 0 19 | 20 | while (i < data.length) { 21 | const size = limiter(Math.random() * (width / 40), 1) 22 | let [offset, max] = maxOffset([data[i], data[i + 1], data[i + 2]]) 23 | const skip = getClosestRoot(Math.random() * sqLen) 24 | 25 | for (let j = 0; j < size; j++) { 26 | for (let k = 0; k < 3; k++) { 27 | if (offset === k) { 28 | ret.push(data[i + k]) 29 | } else { 30 | ret.push(0) 31 | } 32 | } 33 | ret.push(255) 34 | i += 4 35 | } 36 | 37 | for (let j = 0; j < skip; j++) { 38 | ret.push(0) 39 | } 40 | i += skip 41 | } 42 | 43 | const yAxisesCount = (Math.sqrt(data.length) * 4) | 0 44 | for (let i = 0; i < yAxisesCount; i++) { 45 | const swapFrom = getClosestRoot(Math.random() * data.length) 46 | 47 | for (let j = 0; j < 3; j++) { 48 | if (swapFrom < data.length - width * 64) { 49 | for (let k = 0; k < 20; k++) { 50 | const swapPath = swapFrom + j + width * 4 * (k - 4) 51 | if (swapPath > 0) 52 | ret[swapFrom + j + width * 4 * (k - 4)] = ret[swapFrom + j] 53 | } 54 | } 55 | } 56 | } 57 | 58 | return ret 59 | } 60 | -------------------------------------------------------------------------------- /lib/modes/schifty.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (data) { 4 | let size = Math.random() * 1024 * 4; 5 | let total = size; 6 | let temp = []; 7 | const storage = []; 8 | 9 | for (let i = 0; i < data.length; i++) { 10 | if (i < total) temp.push(data[i]); 11 | else { 12 | storage.push(Buffer.from(temp)); 13 | size = Math.random() * 1024 * 2; 14 | total += size; 15 | temp = []; 16 | } 17 | } 18 | 19 | data = Buffer.concat(storage); 20 | return data; 21 | }; 22 | -------------------------------------------------------------------------------- /lib/modes/template: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = function (data, width, height) { 4 | // manipulate data here 5 | 6 | return data 7 | } -------------------------------------------------------------------------------- /lib/modes/vana.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (data) { 4 | const rand = Math.random; 5 | const max = Math.max; 6 | const min = Math.min; 7 | 8 | function giveSeed() { 9 | const seed = [0, 0, 0]; 10 | 11 | const ind1 = Math.floor(rand() * 3); 12 | const ind2 = Math.floor(rand() * 3); 13 | 14 | seed[ind1] = max(rand(), 0.3); 15 | if (rand() > 0.5) seed[ind2] = max(rand(), 0.3); 16 | 17 | return seed; 18 | } 19 | 20 | const seed = giveSeed(); 21 | 22 | for (let i = 0; i < data.length; i += 4) { 23 | /* RED = */ 24 | data[i + 0] = min(data[i] * seed[0] + 100 * seed[2], 255); 25 | /* GREEN = */ 26 | data[i + 1] = min(data[i + 1] * seed[1] + 100 * seed[0], 255); 27 | /* BLUE = */ 28 | data[i + 2] = min(data[i + 2] * seed[2] + 100 * seed[1], 255); 29 | } 30 | 31 | return data; 32 | }; 33 | -------------------------------------------------------------------------------- /lib/modes/vaporwave.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (data) { 4 | const COLORS = [ 5 | [0, 184, 255], 6 | [255, 0, 193], 7 | [150, 0, 255], 8 | [0, 255, 249], 9 | ]; 10 | 11 | for (let i = 0; i < data.length; i += 4) { 12 | if (data[i] <= 15 && data[i + 1] <= 15 && data[i + 2] <= 15) { 13 | data[i] = 0; 14 | data[i + 1] = 0; 15 | data[i + 2] = 0; 16 | } else if ( 17 | data[i] > 15 && 18 | data[i] <= 60 && 19 | data[i + 1] > 15 && 20 | data[i + 1] <= 60 && 21 | data[i + 2] > 15 && 22 | data[i + 2] <= 60 23 | ) { 24 | data[i] = COLORS[0][0]; 25 | data[i + 1] = COLORS[0][1]; 26 | data[i + 2] = COLORS[0][2]; 27 | } else if ( 28 | data[i] > 60 && 29 | data[i] <= 120 && 30 | data[i + 1] > 60 && 31 | data[i + 1] <= 120 && 32 | data[i + 2] > 60 && 33 | data[i + 2] <= 120 34 | ) { 35 | data[i] = COLORS[1][0]; 36 | data[i + 1] = COLORS[1][1]; 37 | data[i + 2] = COLORS[1][2]; 38 | } else if ( 39 | data[i] > 120 && 40 | data[i] <= 180 && 41 | data[i + 1] > 120 && 42 | data[i + 1] <= 180 && 43 | data[i + 2] > 120 && 44 | data[i + 2] <= 180 45 | ) { 46 | data[i] = COLORS[2][0]; 47 | data[i + 1] = COLORS[2][1]; 48 | data[i + 2] = COLORS[2][2]; 49 | } else if ( 50 | data[i] > 180 && 51 | data[i] <= 234 && 52 | data[i + 1] > 180 && 53 | data[i + 1] <= 234 && 54 | data[i + 2] > 180 && 55 | data[i + 2] <= 234 56 | ) { 57 | data[i] = COLORS[3][0]; 58 | data[i + 1] = COLORS[3][1]; 59 | data[i + 2] = COLORS[3][2]; 60 | } else if (data[i] >= 235 && data[i + 1] >= 235 && data[i + 2] >= 235) { 61 | data[i] = 255; 62 | data[i + 1] = 255; 63 | data[i + 2] = 255; 64 | } 65 | } 66 | 67 | return data; 68 | }; 69 | -------------------------------------------------------------------------------- /lib/modes/veneneux.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (data, width, height) { 4 | const rand = Math.random; 5 | const max = Math.max; 6 | 7 | function giveSeed() { 8 | const seed = [0, 0, 0]; 9 | 10 | const ind1 = Math.floor(rand() * 3); 11 | const ind2 = Math.floor(rand() * 3); 12 | 13 | seed[ind1] = max(rand(), 0.1); 14 | if (rand() > 0.5) seed[ind2] = max(rand(), 0.1); 15 | 16 | return seed; 17 | } 18 | 19 | let pixel = 0; 20 | let seed = giveSeed(); 21 | let seedChange = 2; 22 | 23 | for (let i = 0; i < data.length; i += 4) { 24 | if (pixel % (width * 4) === 0) { 25 | seedChange--; 26 | if (seedChange === 0) { 27 | seed = giveSeed(); 28 | seedChange = Math.floor((rand() * height) / 4); 29 | } 30 | } 31 | 32 | data[i + 0] = (data[i + 0] * seed[0] + 1000 * seed[2]) % 256; 33 | data[i + 1] = (data[i + 1] * seed[1] + 1000 * seed[1]) % 256; 34 | data[i + 2] = (data[i + 2] * seed[2] + 1000 * seed[0]) % 256; 35 | data[i + 3] = rand() * 256 - 1; 36 | 37 | pixel++; 38 | } 39 | 40 | return data; 41 | }; 42 | -------------------------------------------------------------------------------- /lib/modes/void.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** Configurable variables */ 4 | const noiseThreshold = 0.2; 5 | const grainThreshold = 0.4; 6 | 7 | /** Math references */ 8 | const min = Math.min, 9 | max = Math.max, 10 | floor = Math.floor, 11 | random = Math.random; 12 | 13 | /** Randomizer */ 14 | function r(min, max) { 15 | return Math.floor(Math.random() * (max - min + 1) + min); 16 | } 17 | 18 | module.exports = function (data) { 19 | for (let index = 0; index < data.length; index += 4) { 20 | const useNoise = random() < noiseThreshold; 21 | const useGrain = random() < grainThreshold ? floor(random() * 50) : 0; 22 | 23 | [0, 1, 2].forEach((i) => { 24 | /** Void effect */ 25 | data[index + i] = data[index + i] - r(1, 15); 26 | data[index + i] = 27 | data[index + i] < 0 ? data[index + i] + 255 : data[index + i]; 28 | 29 | /** Add noise */ 30 | data[index + i] = useNoise 31 | ? min(data[index + i] + r(1, i === 0 ? 15 : 10), 255) 32 | : data[index + i]; 33 | 34 | /** Darken image */ 35 | data[index + i] = min( 36 | 255, 37 | max(0, data[index + i] + floor(random() * 20 - 40)) 38 | ); 39 | 40 | /** Add grain */ 41 | data[index + i] = useGrain 42 | ? min(255, max(0, data[index + i] + useGrain)) 43 | : data[index + i]; 44 | }); 45 | } 46 | 47 | return data; 48 | }; 49 | -------------------------------------------------------------------------------- /lib/modes/walter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = function (data) { 4 | const f = Math.floor, 5 | r = Math.random, 6 | x = Math.max; 7 | 8 | const seed = () => f(r() * 256); 9 | 10 | const hurp = [seed(), seed(), seed()], 11 | lurp = [seed(), seed(), seed()]; 12 | 13 | let multi = f(r() * (255 - x(...hurp, ...lurp))); 14 | 15 | for (let i = 0; i < data.length; i += 4) { 16 | const rP = data[i] / 255, 17 | gP = data[i + 1] / 255, 18 | bP = data[i + 2] / 255; 19 | 20 | if (data[i] < lurp[0] || data[i] > hurp[0]) 21 | data[i] = hurp[0] - lurp[0] + rP * multi; 22 | if (data[i + 1] < lurp[1] || data[i + 1] > hurp[1]) 23 | data[i + 1] = hurp[1] - lurp[1] + gP * multi; 24 | if (data[i + 2] < lurp[2] || data[i + 2] > hurp[2]) 25 | data[1 + 2] = hurp[2] - lurp[2] + bP * multi; 26 | } 27 | 28 | return data; 29 | }; 30 | -------------------------------------------------------------------------------- /lib/mosh.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const encode = require("image-encode"); 4 | const decode = require("image-decode"); 5 | const FileType = require("file-type"); 6 | const { parse, dirname, basename, join } = require("path"); 7 | const { homedir } = require("os"); 8 | const { stat, readFile, writeFile } = require("fs/promises"); 9 | 10 | module.exports = mosh; 11 | 12 | async function mosh(...args) { 13 | // it's cool cuz it's nerdy. 14 | const [err, write, source, mode, ext] = await preflightChecks(...args); 15 | 16 | // callback with error if using cb, else throw 17 | if (err) { 18 | handleError(err, write); 19 | return; 20 | } 21 | 22 | // decode image 23 | const { data, width, height } = decode(source); 24 | 25 | // apply moshes 26 | let imgBuff = Buffer.from(data); 27 | mode.forEach((m) => { 28 | imgBuff = mosh.MODES[m](imgBuff, width, height); 29 | }); 30 | 31 | // encode image 32 | const encodedMosh = encode(imgBuff, [width, height], ext); 33 | 34 | // return data if we aren't using a CB 35 | if (!write) return Buffer.from(encodedMosh); 36 | write(null, Buffer.from(encodedMosh)); 37 | } 38 | 39 | async function preflightChecks(...args) { 40 | let [source, mode, write] = args; 41 | let ext; 42 | 43 | // source validation 44 | const sourceIsBuffer = Buffer.isBuffer(source); 45 | const sourceIsString = typeof source === "string" || source instanceof String; 46 | 47 | if (!source || (!sourceIsString && !sourceIsBuffer)) { 48 | return [ 49 | "Invalid image source -- source must be of type String (path) or Buffer.", 50 | write, 51 | ]; 52 | } 53 | 54 | if (sourceIsString) { 55 | const sourcePath = await validatePath(source); 56 | 57 | if (!sourcePath) { 58 | return [`Invalid source path: ${source}\nFile does not exist.`, write]; 59 | } 60 | 61 | ext = parse(sourcePath).ext.replace(".", ""); 62 | 63 | if (!validateExtension(ext)) { 64 | return [`Invalid file type: ${ext}`, write]; 65 | } 66 | 67 | try { 68 | source = await readFile(sourcePath); 69 | } catch (err) { 70 | return [e.message, write]; 71 | } 72 | } 73 | 74 | // ext validation 75 | if (!ext) { 76 | const fileType = await FileType.fromBuffer(source); 77 | 78 | if (fileType?.ext && validateExtension(fileType?.ext)) { 79 | ext = fileType.ext; 80 | } else { 81 | return [`Invalid file type, requires supported image file.`, write]; 82 | } 83 | } 84 | 85 | // mode validation 86 | const modeIsString = typeof mode === "string" || mode instanceof String; 87 | const modeIsArray = Array.isArray(mode); 88 | 89 | if (mode && !modeIsString && !modeIsArray) { 90 | return [ 91 | `Invalid mode: '${mode}'; mode must be of type String or Array.`, 92 | write, 93 | ]; 94 | } 95 | 96 | if (mode && modeIsString) { 97 | if (!mosh.MODES[mode]) { 98 | return [`Invalid mosh mode: '${mode}'`, write]; 99 | } 100 | 101 | mode = [mode]; 102 | } 103 | 104 | if (mode && modeIsArray) { 105 | let e = []; 106 | mode.forEach((m, i) => { 107 | if (!mosh.MODES[m] && m != null) e.push(m); 108 | 109 | // assign random mode for any null values 110 | if (m === null) mode.splice(i, 1, randomMode()); 111 | }); 112 | 113 | if (e.length > 0) { 114 | return [`Invalid mosh modes: '${e.join(",")}'`, write]; 115 | } 116 | } 117 | 118 | if (!mode) mode = [randomMode()]; 119 | 120 | // write validation 121 | const writeIsString = typeof write === "string" || write instanceof String; 122 | const writeIsCb = write instanceof Function; 123 | 124 | if (write && !writeIsString && !writeIsCb) { 125 | return [`Invalid callback, or write path.`, write]; 126 | } 127 | 128 | if (writeIsString) { 129 | // bubble up path errors 130 | const dir = dirname(write); 131 | const filename = basename(write); 132 | const writePath = await validatePath(dir); 133 | 134 | if (!writePath) { 135 | return [`Invalid write location: ${dir}`, write]; 136 | } 137 | 138 | // prepare write function 139 | write = async (err, data) => { 140 | if (!err) await writeFile(join(writePath, filename), data); 141 | else throw err; 142 | }; 143 | } 144 | 145 | return [null, write, source, mode, ext]; 146 | } 147 | 148 | async function validatePath(fpath) { 149 | // match tilde to homedir 150 | if (fpath.startsWith("~/") || fpath === "~") { 151 | fpath = fpath.replace("~", homedir()); 152 | } 153 | 154 | try { 155 | // stat will reject when the path is invalid 156 | await stat(fpath); 157 | return fpath; 158 | } catch (err) { 159 | return false; 160 | } 161 | } 162 | 163 | function randomMode() { 164 | const modeNames = Object.keys(mosh.MODES); 165 | return modeNames[Math.floor(Math.random() * modeNames.length)]; 166 | } 167 | 168 | function validateExtension(ext) { 169 | return ( 170 | /^jpg$/.test(ext) || 171 | /^jpeg$/.test(ext) || 172 | /^png$/.test(ext) || 173 | /^gif$/.test(ext) || 174 | /^tiff$/.test(ext) || 175 | /^bmp/.test(ext) 176 | ); 177 | } 178 | 179 | function handleError(msg, cb) { 180 | const usingCb = cb && cb instanceof Function; 181 | const error = new Error(msg); 182 | 183 | if (usingCb) cb(error); 184 | else throw error; 185 | } 186 | 187 | mosh.MODES = { 188 | abna: require('./modes/abna'), 189 | blurbobb: require("./modes/blurbobb"), 190 | gazette: require("./modes/gazette"), 191 | manticore95: require("./modes/manticore95"), 192 | schifty: require("./modes/schifty"), 193 | veneneux: require("./modes/veneneux"), 194 | vana: require("./modes/vana"), 195 | fatcat: require("./modes/fatcat"), 196 | vaporwave: require("./modes/vaporwave"), 197 | void: require("./modes/void"), 198 | chimera: require("./modes/chimera"), 199 | walter: require("./modes/walter"), 200 | castles: require("./modes/castles"), 201 | }; 202 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "datamosh", 3 | "version": "2.1.4", 4 | "description": "Edit images via buffers.", 5 | "keywords": [ 6 | "datamosh", 7 | "mosh", 8 | "jpg", 9 | "png", 10 | "tiff", 11 | "bmp", 12 | "gif", 13 | "edit", 14 | "image", 15 | "buffer", 16 | "glitch" 17 | ], 18 | "engines": { 19 | "node": ">=14.0.0" 20 | }, 21 | "author": "Michael Sterpka ", 22 | "contributors": [ 23 | "Tyler Laskey (https://github.com/tlaskey)", 24 | "BonjourInternet (https://github.com/BjrInt)" 25 | ], 26 | "license": "MIT", 27 | "repository": { 28 | "type": "git", 29 | "url": "https://github.com/mster/datamosh.git" 30 | }, 31 | "bugs": { 32 | "url": "https://github.com/mster/datamosh/issues" 33 | }, 34 | "main": "index.js", 35 | "scripts": { 36 | "test": "npm run lint && npm run jest", 37 | "jest": "jest ./tests", 38 | "lint": "echo \"Linting with Prettier\n\" && npx prettier --check . && echo \"Lint passed\n\"", 39 | "lint-fix": "echo \"Auto-fixing with Prettier...\" && npx prettier --write ." 40 | }, 41 | "jest": { 42 | "testRegex": "tests/.*.test.js$" 43 | }, 44 | "dependencies": { 45 | "file-type": "^16.5.0", 46 | "image-decode": "^1.2.2", 47 | "image-encode": "^1.3.1" 48 | }, 49 | "devDependencies": { 50 | "jest": "^27.0.4", 51 | "prettier": "^2.3.0" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /tests/fixtures/a.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Datamosh-js/datamosh/544a008a758e2db193f6949c6aac6a735710a487/tests/fixtures/a.txt -------------------------------------------------------------------------------- /tests/fixtures/rgb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Datamosh-js/datamosh/544a008a758e2db193f6949c6aac6a735710a487/tests/fixtures/rgb.png -------------------------------------------------------------------------------- /tests/preflight.test.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { join } = require("path"); 4 | const dm = require("../index"); 5 | 6 | const tryCatchWrap = async (...args) => { 7 | let res; 8 | try { 9 | res = await dm(...args); 10 | } catch (e) { 11 | res = e; 12 | } 13 | return res; 14 | }; 15 | 16 | describe("Preflight source checks should", () => { 17 | test("throw on invalid source type.", async () => { 18 | const obj = {}; 19 | const arr = [1, 2, 3]; 20 | 21 | let objErr = await tryCatchWrap(obj); 22 | 23 | expect(objErr.constructor).toBe(Error); 24 | expect(objErr.message).toBe( 25 | "Invalid image source -- source must be of type String (path) or Buffer." 26 | ); 27 | 28 | let arrErr = await tryCatchWrap(arr); 29 | 30 | expect(arrErr.constructor).toBe(Error); 31 | expect(arrErr.message).toBe( 32 | "Invalid image source -- source must be of type String (path) or Buffer." 33 | ); 34 | }); 35 | 36 | test("throw on bad source buffer", async () => { 37 | const badImg = Buffer.from(["ff", "0f", "0f", "ff"]); 38 | 39 | let buffErr = await tryCatchWrap(badImg); 40 | 41 | expect(buffErr.constructor).toBe(Error); 42 | expect(buffErr.message).toBe( 43 | "Invalid file type, requires supported image file." 44 | ); 45 | }); 46 | 47 | test("throw on unsupported file type.", async () => { 48 | const txt = join(__dirname, "/fixtures/a.txt"); 49 | 50 | let txtErr = await tryCatchWrap(txt); 51 | 52 | expect(txtErr.constructor).toBe(Error); 53 | expect(txtErr.message).toBe("Invalid file type: txt"); 54 | }); 55 | 56 | test("throw on file not found", async () => { 57 | const path = "/does/not/exist.jpg"; 58 | 59 | let enoent = await tryCatchWrap(path); 60 | 61 | expect(enoent.constructor).toBe(Error); 62 | expect(enoent.message).toBe( 63 | `Invalid source path: ${path}\nFile does not exist.` 64 | ); 65 | }); 66 | }); 67 | 68 | describe("Preflight mode checks should", () => { 69 | test("throw on invalid mosh (as string)", async () => { 70 | const imgPath = join(__dirname, "/fixtures/rgb.png"); 71 | const badMode = "thisIsNotAModeName"; 72 | 73 | let modeErr = await tryCatchWrap(imgPath, badMode); 74 | 75 | expect(modeErr.constructor).toBe(Error); 76 | expect(modeErr.message).toBe(`Invalid mosh mode: '${badMode}'`); 77 | }); 78 | 79 | test("throw on invalid mosh (as array)", async () => { 80 | const imgPath = join(__dirname, "/fixtures/rgb.png"); 81 | const badModes = ["thisIsNotAModeName", "anotherBadOne"]; 82 | 83 | let modeErr = await tryCatchWrap(imgPath, badModes); 84 | 85 | expect(modeErr.constructor).toBe(Error); 86 | expect(modeErr.message).toBe(`Invalid mosh modes: '${badModes}'`); 87 | }); 88 | 89 | test("choose a mode when one isn't provided", async () => { 90 | const imgPath = join(__dirname, "/fixtures/rgb.png"); 91 | 92 | let imgBuff = await tryCatchWrap(imgPath); 93 | 94 | expect(imgBuff.constructor).toBe(Buffer); 95 | }); 96 | }); 97 | 98 | describe("Preflight write checks should", () => { 99 | test("throw on invalid write argument", async () => { 100 | const imgPath = join(__dirname, "/fixtures/rgb.png"); 101 | const obj = {}, 102 | arr = [1, 2, 3]; 103 | 104 | let objErr = await tryCatchWrap(imgPath, null, obj); 105 | 106 | expect(objErr.constructor).toBe(Error); 107 | expect(objErr.message).toBe(`Invalid callback, or write path.`); 108 | 109 | let arrErr = await tryCatchWrap(imgPath, null, arr); 110 | 111 | expect(arrErr.constructor).toBe(Error); 112 | expect(arrErr.message).toBe(`Invalid callback, or write path.`); 113 | }); 114 | 115 | test("throw on bad write path", async () => { 116 | const path = "/does/not/exist.jpg"; 117 | 118 | let badWrite = await tryCatchWrap(path); 119 | 120 | expect(badWrite.constructor).toBe(Error); 121 | expect(badWrite.message).toBe( 122 | `Invalid source path: ${path}\nFile does not exist.` 123 | ); 124 | }); 125 | }); 126 | --------------------------------------------------------------------------------