├── test-images ├── dice.png ├── dice.qoi ├── kodim10.png ├── kodim10.qoi ├── kodim23.png ├── kodim23.qoi ├── qoi_logo.png ├── qoi_logo.qoi ├── testcard.png ├── testcard.qoi ├── checkedboard.png ├── checkerboard.qoi ├── testcard_rgba.png ├── testcard_rgba.qoi ├── wikipedia_008.png └── wikipedia_008.qoi ├── src ├── qoi.js ├── decode.js └── encode.js ├── package.json ├── LICENSE ├── .gitignore └── README.md /test-images/dice.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/dice.png -------------------------------------------------------------------------------- /test-images/dice.qoi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/dice.qoi -------------------------------------------------------------------------------- /test-images/kodim10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/kodim10.png -------------------------------------------------------------------------------- /test-images/kodim10.qoi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/kodim10.qoi -------------------------------------------------------------------------------- /test-images/kodim23.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/kodim23.png -------------------------------------------------------------------------------- /test-images/kodim23.qoi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/kodim23.qoi -------------------------------------------------------------------------------- /test-images/qoi_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/qoi_logo.png -------------------------------------------------------------------------------- /test-images/qoi_logo.qoi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/qoi_logo.qoi -------------------------------------------------------------------------------- /test-images/testcard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/testcard.png -------------------------------------------------------------------------------- /test-images/testcard.qoi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/testcard.qoi -------------------------------------------------------------------------------- /test-images/checkedboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/checkedboard.png -------------------------------------------------------------------------------- /test-images/checkerboard.qoi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/checkerboard.qoi -------------------------------------------------------------------------------- /test-images/testcard_rgba.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/testcard_rgba.png -------------------------------------------------------------------------------- /test-images/testcard_rgba.qoi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/testcard_rgba.qoi -------------------------------------------------------------------------------- /test-images/wikipedia_008.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/wikipedia_008.png -------------------------------------------------------------------------------- /test-images/wikipedia_008.qoi: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kchapelier/qoijs/HEAD/test-images/wikipedia_008.qoi -------------------------------------------------------------------------------- /src/qoi.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const QOI = {}; 4 | 5 | QOI.decode = require('./decode'); 6 | QOI.encode = require('./encode'); 7 | 8 | module.exports = QOI; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "qoi", 3 | "version": "1.0.0", 4 | "description": "Quite-OK Image format encoder/decoder in vanilla javascript", 5 | "main": "src/qoi.js", 6 | "scripts": { 7 | "build": "browserify src/qoi.js --s QOI > build/qoijs.js", 8 | "build-min": "browserify src/qoi.js --s QOI | terser --ecma 5 > build/qoijs.min.js" 9 | }, 10 | "files": [ 11 | "src/qoi.js", 12 | "src/encode.js", 13 | "src/decode.js" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/kchapelier/qoijs.git" 18 | }, 19 | "keywords": [ 20 | "qoi", 21 | "quite-ok image", 22 | "rgb", 23 | "rgba", 24 | "image", 25 | "picture", 26 | "encode", 27 | "decode" 28 | ], 29 | "author": "Kevin Chapelier", 30 | "license": "MIT", 31 | "readmeFilename": "README.md", 32 | "bugs": { 33 | "url": "https://github.com/kchapelier/qoijs/issues" 34 | }, 35 | "homepage": "https://github.com/kchapelier/qoijs", 36 | "devDependencies": { 37 | "browserify": "^16.5.1", 38 | "pngjs": "^6.0.0", 39 | "terser": "^4.6.13" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Kevin Chapelier 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | 106 | # IDE 107 | .vscode/ 108 | .idea/ 109 | 110 | # Project specific 111 | test-scripts/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # qoijs 2 | 3 | [![NPM version](https://badge.fury.io/js/qoijs.svg)](http://badge.fury.io/js/qoijs) 4 | 5 | "[Quite-OK Image format](https://qoiformat.org/)" encoder/decoder in vanilla javascript. 6 | 7 | ## Installing 8 | 9 | With [npm](https://www.npmjs.com/) do: 10 | 11 | ``` 12 | npm install qoijs 13 | ``` 14 | 15 | With [yarn](https://yarnpkg.com/) do: 16 | 17 | ``` 18 | yarn add qoijs 19 | ``` 20 | 21 | A compiled version for web browsers is also available on a CDN: 22 | 23 | ```html 24 | 25 | ``` 26 | 27 | ## Public API 28 | 29 | ### Functions 30 | 31 | **decode(arrayBuffer[, byteOffset[, byteLength[, outputChannels]]])** 32 | 33 | Decode a QOI file given as an ArrayBuffer. 34 | 35 | Returns a literal containing the color data as a TypedArray and all the metadata of the image. 36 | 37 | - *arrayBuffer:* ArrayBuffer containing the QOI file. 38 | - *byteOffset:* Offset to the start of the QOI file in arrayBuffer, defaults to the 0. 39 | - *byteLength:* Length of the QOI file in bytes, defaults to the remaining length of arrayBuffer after byteOffset. 40 | - *outputChannels:* Number of channels to include in the decoded array (used to remove or add the alpha channel), defaults to the number of channels contained in the QOI file. 41 | 42 | ```js 43 | const QOI = require('qoijs'); 44 | 45 | // using fs in node to read the content of a file as a Buffer 46 | const buffer = require('fs').readFileSync('some_image.qoi'); 47 | const decodedData = QOI.decode(buffer.buffer, buffer.byteOffset, buffer.byteLength); 48 | ``` 49 | 50 | ```js 51 | // using FileReader to read a file retrieved from a drop event as an ArrayBuffer 52 | const reader = new FileReader(); 53 | reader.readAsArrayBuffer(file); 54 | reader.onloadend = function () { 55 | const arrayBuffer = reader.result; 56 | const decodedData = QOI.decode(arrayBuffer); 57 | }; 58 | ``` 59 | 60 | **encode(colorData, description)** 61 | 62 | Encode a QOI file. 63 | 64 | Returns an ArrayBuffer containing the QOI file content. 65 | 66 | - *colorData:* Flat array containing the color information for each pixel of the image (from left to right, top to bottom). Must be either an instance of Uint8Array or Uint8ClampedArray. 67 | - *description:* 68 | - *width:* The width of the image. 69 | - *height:* The height of the image. 70 | - *channels:* The number of channels of the image. 71 | - 3: RGB, 4: RGBA 72 | - *colorspace:* The color space used to encode the colors in colorData. 73 | - 0: sRGB with linear alpha, 1: linear 74 | 75 | ```js 76 | const QOI = require('qoijs'); 77 | 78 | // encode a 2x2 b/w checkerboard pattern from an arbitrary colorData array 79 | const colorData = new Uint8Array([0,0,0,255, 255,255,255,255, 255,255,255,255, 0,0,0,0]); 80 | const arrayBuffer = QOI.encode(colorData, { 81 | width: 2, 82 | height: 2, 83 | channels: 4, 84 | colorspace: 0 85 | }); 86 | ``` 87 | 88 | ```js 89 | // encode the content of a 2D canvas (ImageData) 90 | const imageData = canvasContext.getImageData(0, 0, canvas.width, canvas.height); 91 | const arrayBuffer = QOI.encode(imageData.data, { 92 | width: imageData.width, 93 | height: imageData.height, 94 | channels: 4, 95 | colorspace: 0 96 | }); 97 | ``` 98 | 99 | ## History 100 | 101 | ### [1.0.0](https://github.com/kchapelier/qoijs/tree/1.0.0) (2021-12-27) : 102 | 103 | - First release 104 | 105 | ## How to contribute ? 106 | 107 | For new features and other enhancements, please make sure to contact me beforehand, either on [Twitter](https://twitter.com/kchplr) or through an issue on Github. 108 | 109 | ## License 110 | 111 | MIT -------------------------------------------------------------------------------- /src/decode.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Decode a QOI file given as an ArrayBuffer. 5 | * 6 | * @param {ArrayBuffer} arrayBuffer ArrayBuffer containing the QOI file. 7 | * @param {int|null} [byteOffset] Offset to the start of the QOI file in arrayBuffer 8 | * @param {int|null} [byteLength] Length of the QOI file in bytes 9 | * @param {int|null} [outputChannels] Number of channels to include in the decoded array 10 | * 11 | * @returns {{channels: number, data: Uint8Array, colorspace: number, width: number, error: boolean, height: number}} 12 | */ 13 | function decode (arrayBuffer, byteOffset, byteLength, outputChannels) { 14 | if (typeof byteOffset === 'undefined' || byteOffset === null) { 15 | byteOffset = 0; 16 | } 17 | 18 | if (typeof byteLength === 'undefined' || byteLength === null) { 19 | byteLength = arrayBuffer.byteLength - byteOffset; 20 | } 21 | 22 | const uint8 = new Uint8Array(arrayBuffer, byteOffset, byteLength); 23 | 24 | const magic1 = uint8[0]; 25 | const magic2 = uint8[1]; 26 | const magic3 = uint8[2]; 27 | const magic4 = uint8[3]; 28 | 29 | const width = ((uint8[4] << 24) | (uint8[5] << 16) | (uint8[6] << 8) | uint8[7]) >>> 0; 30 | const height = ((uint8[8] << 24) | (uint8[9] << 16) | (uint8[10] << 8) | uint8[11]) >>> 0; 31 | 32 | const channels = uint8[12]; 33 | const colorspace = uint8[13]; 34 | 35 | if (typeof outputChannels === 'undefined' || outputChannels === null) { 36 | outputChannels = channels; 37 | } 38 | 39 | if (magic1 !== 0x71 || magic2 !== 0x6F || magic3 !== 0x69 || magic4 !== 0x66) { 40 | throw new Error('QOI.decode: The signature of the QOI file is invalid'); 41 | } 42 | 43 | if (channels < 3 || channels > 4) { 44 | throw new Error('QOI.decode: The number of channels declared in the file is invalid'); 45 | } 46 | 47 | if (colorspace > 1) { 48 | throw new Error('QOI.decode: The colorspace declared in the file is invalid'); 49 | } 50 | 51 | if (outputChannels < 3 || outputChannels > 4) { 52 | throw new Error('QOI.decode: The number of channels for the output is invalid'); 53 | } 54 | 55 | const pixelLength = width * height * outputChannels; 56 | const result = new Uint8Array(pixelLength); 57 | 58 | let arrayPosition = 14; 59 | 60 | const index = new Uint8Array(64 * 4); 61 | let indexPosition = 0; 62 | 63 | let red = 0; 64 | let green = 0; 65 | let blue = 0; 66 | let alpha = 255; 67 | 68 | const chunksLength = byteLength - 8; 69 | 70 | let run = 0; 71 | let pixelPosition = 0; 72 | 73 | for (; pixelPosition < pixelLength && arrayPosition < byteLength - 4; pixelPosition += outputChannels) { 74 | if (run > 0) { 75 | run--; 76 | } else if (arrayPosition < chunksLength) { 77 | const byte1 = uint8[arrayPosition++]; 78 | 79 | if (byte1 === 0b11111110) { // QOI_OP_RGB 80 | red = uint8[arrayPosition++]; 81 | green = uint8[arrayPosition++]; 82 | blue = uint8[arrayPosition++]; 83 | } else if (byte1 === 0b11111111) { // QOI_OP_RGBA 84 | red = uint8[arrayPosition++]; 85 | green = uint8[arrayPosition++]; 86 | blue = uint8[arrayPosition++]; 87 | alpha = uint8[arrayPosition++]; 88 | } else if ((byte1 & 0b11000000) === 0b00000000) { // QOI_OP_INDEX 89 | red = index[byte1 * 4]; 90 | green = index[byte1 * 4 + 1]; 91 | blue = index[byte1 * 4 + 2]; 92 | alpha = index[byte1 * 4 + 3]; 93 | } else if ((byte1 & 0b11000000) === 0b01000000) { // QOI_OP_DIFF 94 | red += ((byte1 >> 4) & 0b00000011) - 2; 95 | green += ((byte1 >> 2) & 0b00000011) - 2; 96 | blue += (byte1 & 0b00000011) - 2; 97 | 98 | // handle wraparound 99 | red = (red + 256) % 256; 100 | green = (green + 256) % 256; 101 | blue = (blue + 256) % 256; 102 | } else if ((byte1 & 0b11000000) === 0b10000000) { // QOI_OP_LUMA 103 | const byte2 = uint8[arrayPosition++]; 104 | const greenDiff = (byte1 & 0b00111111) - 32; 105 | const redDiff = greenDiff + ((byte2 >> 4) & 0b00001111) - 8; 106 | const blueDiff = greenDiff + (byte2 & 0b00001111) - 8; 107 | 108 | // handle wraparound 109 | red = (red + redDiff + 256) % 256; 110 | green = (green + greenDiff + 256) % 256; 111 | blue = (blue + blueDiff + 256) % 256; 112 | } else if ((byte1 & 0b11000000) === 0b11000000) { // QOI_OP_RUN 113 | run = byte1 & 0b00111111; 114 | } 115 | 116 | indexPosition = ((red * 3 + green * 5 + blue * 7 + alpha * 11) % 64) * 4; 117 | index[indexPosition] = red; 118 | index[indexPosition + 1] = green; 119 | index[indexPosition + 2] = blue; 120 | index[indexPosition + 3] = alpha; 121 | } 122 | 123 | if (outputChannels === 4) { // RGBA 124 | result[pixelPosition] = red; 125 | result[pixelPosition + 1] = green; 126 | result[pixelPosition + 2] = blue; 127 | result[pixelPosition + 3] = alpha; 128 | } else { // RGB 129 | result[pixelPosition] = red; 130 | result[pixelPosition + 1] = green; 131 | result[pixelPosition + 2] = blue; 132 | } 133 | } 134 | 135 | if (pixelPosition < pixelLength) { 136 | throw new Error('QOI.decode: Incomplete image'); 137 | } 138 | 139 | // checking the 00000001 padding is not required, as per specs 140 | 141 | return { 142 | width: width, 143 | height: height, 144 | colorspace: colorspace, 145 | channels: outputChannels, 146 | data: result 147 | }; 148 | } 149 | 150 | module.exports = decode; -------------------------------------------------------------------------------- /src/encode.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /** 4 | * Encode a QOI file. 5 | * 6 | * @param {Uint8Array|Uint8ClampedArray} colorData Array containing the color information for each pixel of the image (left to right, top to bottom) 7 | * @param {object} description 8 | * @param {int} description.width Width of the image 9 | * @param {int} description.height Height of the image 10 | * @param {int} description.channels Number of channels in the image (3: RGB, 4: RGBA) 11 | * @param {int} description.colorspace Colorspace used in the image (0: sRGB with linear alpha, 1: linear) 12 | * 13 | * @returns {ArrayBuffer} ArrayBuffer containing the QOI file content 14 | */ 15 | function encode (colorData, description) { 16 | const width = description.width; 17 | const height = description.height; 18 | const channels = description.channels; 19 | const colorspace = description.colorspace; 20 | 21 | let red = 0; 22 | let green = 0; 23 | let blue = 0; 24 | let alpha = 255; 25 | let prevRed = red; 26 | let prevGreen = green; 27 | let prevBlue = blue; 28 | let prevAlpha = alpha; 29 | 30 | let run = 0; 31 | let p = 0; 32 | const pixelLength = width * height * channels; 33 | const pixelEnd = pixelLength - channels; 34 | 35 | if (width < 0 || width >= 4294967296) { 36 | throw new Error('QOI.encode: Invalid description.width'); 37 | } 38 | 39 | if (height < 0 || height >= 4294967296) { 40 | throw new Error('QOI.encode: Invalid description.height'); 41 | } 42 | 43 | if (colorData.constructor.name !== 'Uint8Array' && colorData.constructor.name !== 'Uint8ClampedArray') { 44 | throw new Error('QOI.encode: The provided colorData must be instance of Uint8Array or Uint8ClampedArray'); 45 | } 46 | 47 | if (colorData.length !== pixelLength) { 48 | throw new Error('QOI.encode: The length of colorData is incorrect'); 49 | } 50 | 51 | if (channels !== 3 && channels !== 4) { 52 | throw new Error('QOI.encode: Invalid description.channels, must be 3 or 4'); 53 | } 54 | 55 | if (colorspace !== 0 && colorspace !== 1) { 56 | throw new Error('QOI.encode: Invalid description.colorspace, must be 0 or 1'); 57 | } 58 | 59 | const maxSize = width * height * (channels + 1) + 14 + 8; 60 | const result = new Uint8Array(maxSize); 61 | const index = new Uint8Array(64 * 4); 62 | 63 | // 0->3 : magic "qoif" 64 | result[p++] = 0x71; 65 | result[p++] = 0x6F; 66 | result[p++] = 0x69; 67 | result[p++] = 0x66; 68 | 69 | // 4->7 : width 70 | result[p++] = (width >> 24) & 0xFF; 71 | result[p++] = (width >> 16) & 0xFF; 72 | result[p++] = (width >> 8) & 0xFF; 73 | result[p++] = width & 0xFF; 74 | 75 | // 8->11 : height 76 | result[p++] = (height >> 24) & 0xFF; 77 | result[p++] = (height >> 16) & 0xFF; 78 | result[p++] = (height >> 8) & 0xFF; 79 | result[p++] = height & 0xFF; 80 | 81 | // 12 : channels, 13 : colorspace 82 | result[p++] = channels; 83 | result[p++] = colorspace; 84 | 85 | for (let pixelPos = 0; pixelPos < pixelLength; pixelPos += channels) { 86 | if (channels === 4) { 87 | red = colorData[pixelPos]; 88 | green = colorData[pixelPos + 1]; 89 | blue = colorData[pixelPos + 2]; 90 | alpha = colorData[pixelPos + 3]; 91 | } else { 92 | red = colorData[pixelPos]; 93 | green = colorData[pixelPos + 1]; 94 | blue = colorData[pixelPos + 2]; 95 | } 96 | 97 | if (prevRed === red && prevGreen === green && prevBlue === blue && prevAlpha === alpha) { 98 | run++; 99 | 100 | // reached the maximum run length, or reached the end of colorData 101 | if (run === 62 || pixelPos === pixelEnd) { 102 | // QOI_OP_RUN 103 | result[p++] = 0b11000000 | (run - 1); 104 | run = 0; 105 | } 106 | } else { 107 | if (run > 0) { 108 | // QOI_OP_RUN 109 | result[p++] = 0b11000000 | (run - 1); 110 | run = 0; 111 | } 112 | 113 | const indexPosition = ((red * 3 + green * 5 + blue * 7 + alpha * 11) % 64) * 4; 114 | 115 | if (index[indexPosition] === red && index[indexPosition + 1] === green && index[indexPosition + 2] === blue && index[indexPosition + 3] === alpha) { 116 | result[p++] = indexPosition / 4; 117 | } else { 118 | index[indexPosition] = red; 119 | index[indexPosition + 1] = green; 120 | index[indexPosition + 2] = blue; 121 | index[indexPosition + 3] = alpha; 122 | 123 | if (alpha === prevAlpha) { 124 | // ternary with bitmask handles the wraparound 125 | let vr = red - prevRed; 126 | vr = vr & 0b10000000 ? (vr - 256) % 256 : (vr + 256) % 256; 127 | let vg = green - prevGreen; 128 | vg = vg & 0b10000000 ? (vg - 256) % 256 : (vg + 256) % 256; 129 | let vb = blue - prevBlue; 130 | vb = vb & 0b10000000 ? (vb - 256) % 256 : (vb + 256) % 256; 131 | 132 | const vg_r = vr - vg; 133 | const vg_b = vb - vg; 134 | 135 | if (vr > -3 && vr < 2 && vg > -3 && vg < 2 && vb > -3 && vb < 2) { 136 | // QOI_OP_DIFF 137 | result[p++] = 0b01000000 | (vr + 2) << 4 | (vg + 2) << 2 | (vb + 2); 138 | } else if (vg_r > -9 && vg_r < 8 && vg > -33 && vg < 32 && vg_b > -9 && vg_b < 8) { 139 | // QOI_OP_LUMA 140 | result[p++] = 0b10000000 | (vg + 32); 141 | result[p++] = (vg_r + 8) << 4 | (vg_b + 8); 142 | } else { 143 | // QOI_OP_RGB 144 | result[p++] = 0b11111110; 145 | result[p++] = red; 146 | result[p++] = green; 147 | result[p++] = blue; 148 | } 149 | } else { 150 | // QOI_OP_RGBA 151 | result[p++] = 0b11111111; 152 | result[p++] = red; 153 | result[p++] = green; 154 | result[p++] = blue; 155 | result[p++] = alpha; 156 | } 157 | } 158 | } 159 | 160 | prevRed = red; 161 | prevGreen = green; 162 | prevBlue = blue; 163 | prevAlpha = alpha; 164 | } 165 | 166 | // 00000001 end marker/padding 167 | result[p++] = 0; 168 | result[p++] = 0; 169 | result[p++] = 0; 170 | result[p++] = 0; 171 | result[p++] = 0; 172 | result[p++] = 0; 173 | result[p++] = 0; 174 | result[p++] = 1; 175 | 176 | // return an ArrayBuffer trimmed to the correct length 177 | return result.buffer.slice(0, p); 178 | } 179 | 180 | module.exports = encode; --------------------------------------------------------------------------------