├── test ├── sample.jpg ├── sample.png ├── samplewithmeta.png ├── sampleGIFDataURI.txt ├── samplePNGDataURI.txt └── index.test.js ├── babel.config.js ├── .release-it.json ├── .eslintrc.js ├── types ├── main.d.ts.map └── main.d.ts ├── rollup.config.js ├── tsconfig.json ├── .github └── workflows │ ├── lint.yaml │ └── test.yaml ├── package.json ├── README.md ├── .gitignore └── src └── main.js /test/sample.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucach/meta-png/HEAD/test/sample.jpg -------------------------------------------------------------------------------- /test/sample.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucach/meta-png/HEAD/test/sample.png -------------------------------------------------------------------------------- /test/samplewithmeta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucach/meta-png/HEAD/test/samplewithmeta.png -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [['@babel/preset-env', {targets: {node: 'current'}}]], 3 | }; -------------------------------------------------------------------------------- /.release-it.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": { 3 | "release": true 4 | }, 5 | "hooks": { 6 | "after:bump": "npm run build" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | jest: true, 7 | }, 8 | extends: [ 9 | 'airbnb-base', 10 | ], 11 | parserOptions: { 12 | ecmaVersion: 12, 13 | sourceType: 'module', 14 | }, 15 | rules: { 16 | }, 17 | }; 18 | -------------------------------------------------------------------------------- /types/main.d.ts.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"main.d.ts","sourceRoot":"","sources":["../src/main.js"],"names":[],"mappings":"AAqEA;;;;;;;;EAQE;AACF,2CALW,UAAU,OACV,MAAM,SACN,MAAM,GACJ,UAAU,CA0BtB;AAED;;;;;;;;;EASE;AACF,sDANW,MAAM,OAEN,MAAM,SACN,MAAM,GACJ,MAAM,CAclB;AAED;;;;;;;;EAQE;AACF,2CALW,UAAU,OACV,MAAM,GACJ,MAAM,GAAC,SAAS,CAkB5B"} -------------------------------------------------------------------------------- /test/sampleGIFDataURI.txt: -------------------------------------------------------------------------------- 1 | data:image/gif;base64,R0lGODlhEAAQAMQAAORHHOVSKudfOulrSOp3WOyDZu6QdvCchPGolfO0o/XBs/fNwfjZ0frl3/zy7////wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACH5BAkAABAALAAAAAAQABAAAAVVICSOZGlCQAosJ6mu7fiyZeKqNKToQGDsM8hBADgUXoGAiqhSvp5QAnQKGIgUhwFUYLCVDFCrKUE1lBavAViFIDlTImbKC5Gm2hB0SlBCBMQiB0UjIQA7 -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import { terser } from 'rollup-plugin-terser'; 2 | import pkg from './package.json'; 3 | 4 | export default [ 5 | // browser-friendly UMD build 6 | { 7 | input: 'src/main.js', 8 | output: { 9 | name: 'MetaPNG', 10 | file: pkg.main, 11 | format: 'umd', 12 | }, 13 | plugins: [ 14 | terser(), 15 | ], 16 | }, 17 | 18 | ]; 19 | -------------------------------------------------------------------------------- /test/samplePNGDataURI.txt: -------------------------------------------------------------------------------- 1 | data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAJYAAABLCAYAAACSoX4TAAABXklEQVR4Xu3SMQ0AAAzDsJU/6aHI5wLoEXlnCgQFFny6VODAgiApAFaS1SlYDCQFwEqyOgWLgaQAWElWp2AxkBQAK8nqFCwGkgJgJVmdgsVAUgCsJKtTsBhICoCVZHUKFgNJAbCSrE7BYiApAFaS1SlYDCQFwEqyOgWLgaQAWElWp2AxkBQAK8nqFCwGkgJgJVmdgsVAUgCsJKtTsBhICoCVZHUKFgNJAbCSrE7BYiApAFaS1SlYDCQFwEqyOgWLgaQAWElWp2AxkBQAK8nqFCwGkgJgJVmdgsVAUgCsJKtTsBhICoCVZHUKFgNJAbCSrE7BYiApAFaS1SlYDCQFwEqyOgWLgaQAWElWp2AxkBQAK8nqFCwGkgJgJVmdgsVAUgCsJKtTsBhICoCVZHUKFgNJAbCSrE7BYiApAFaS1SlYDCQFwEqyOgWLgaQAWElWp2AxkBQAK8nqFCwGkgIP1GsATGM1Cy0AAAAASUVORK5CYII= -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["src/**/*"], 3 | "compilerOptions": { 4 | // Tells TypeScript to read JS files, as 5 | // normally they are ignored as source files 6 | "allowJs": true, 7 | // Generate d.ts files 8 | "declaration": true, 9 | // This compiler run should 10 | // only output d.ts files 11 | "emitDeclarationOnly": true, 12 | // Types should go into this directory. 13 | // Removing this would place the .d.ts files 14 | // next to the .js files 15 | "outDir": "types", 16 | // go to js file when using IDE functions like 17 | // "Go to Definition" in VSCode 18 | "declarationMap": true 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | # This workflow installs node dependencies (using the cache if package-lock.json has not been modified) and lints 2 | name: Lint 3 | 4 | on: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js 15.x 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 15.x 21 | - name: Cache node modules 22 | uses: actions/cache@v2 23 | with: 24 | path: ~/.npm 25 | key: npm-${{ hashFiles('**/package-lock.json') }} 26 | - name: Install dependecies 27 | run: npm install 28 | - name: Lint 29 | run: npm run lint -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # This workflow installs node dependencies (using the cache if package-lock.json has not been modified) and runs the tests 2 | name: Test 3 | 4 | on: 5 | push: 6 | branches: [ main ] 7 | pull_request: 8 | branches: [ main ] 9 | 10 | jobs: 11 | build: 12 | 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/checkout@v2 17 | - name: Use Node.js 15.x 18 | uses: actions/setup-node@v1 19 | with: 20 | node-version: 15.x 21 | - name: Cache node modules 22 | uses: actions/cache@v2 23 | with: 24 | path: ~/.npm 25 | key: npm-${{ hashFiles('**/package-lock.json') }} 26 | - name: Install dependecies 27 | run: npm install 28 | - name: Test 29 | run: npm run test 30 | - name: Coverage/Codecov 31 | run: npm run test:codecov -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "meta-png", 3 | "version": "1.0.6", 4 | "description": "Write and read metadata in PNG files.", 5 | "main": "dist/meta-png.umd.js", 6 | "files": [ 7 | "dist", 8 | "types/main.d.ts" 9 | ], 10 | "repository": "https://github.com/lucach/meta-png.git", 11 | "scripts": { 12 | "test": "jest", 13 | "test:coverage": "npm run test -- --ci --coverage", 14 | "test:codecov": "npm run test:coverage && codecov", 15 | "lint": "eslint src/*", 16 | "build": "rollup --config", 17 | "release": "release-it" 18 | }, 19 | "keywords": [ 20 | "metadata", 21 | "png" 22 | ], 23 | "author": { 24 | "name": "Luca Chiodini", 25 | "email": "luca.chiodini@usi.ch" 26 | }, 27 | "license": "ISC", 28 | "types": "./types/main.d.ts", 29 | "devDependencies": { 30 | "@babel/core": "^7.12.13", 31 | "@babel/preset-env": "^7.12.13", 32 | "@rollup/plugin-commonjs": "^17.1.0", 33 | "@rollup/plugin-node-resolve": "^11.1.1", 34 | "babel-jest": "^26.6.3", 35 | "codecov": "^3.8.1", 36 | "eslint": "^7.19.0", 37 | "eslint-config-airbnb-base": "^14.2.1", 38 | "eslint-plugin-import": "^2.22.1", 39 | "exifr": "^6.0.0", 40 | "jest": "^26.6.3", 41 | "lodash": "^4.17.20", 42 | "release-it": "^14.4.0", 43 | "rollup": "^2.39.0", 44 | "rollup-plugin-terser": "^7.0.2", 45 | "typescript": "^4.7.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # meta-png 2 | [![NPM version](https://badge.fury.io/js/meta-png.svg)](https://www.npmjs.com/package/meta-png) 3 | [![codecov](https://codecov.io/gh/lucach/meta-png/branch/main/graph/badge.svg?token=ZGEFAO5WDP)](https://codecov.io/gh/lucach/meta-png) 4 | ![Test](https://github.com/lucach/meta-png/workflows/Test/badge.svg) 5 | ![Lint](https://github.com/lucach/meta-png/workflows/Lint/badge.svg) 6 | 7 | Simple, zero-dependencies NodeJS/JavaScript library to store and retrieve metadata in PNG files. 8 | 9 | ## Installation 10 | Use your favourite package manager 11 | ```bash 12 | npm install meta-png 13 | # or: yarn add meta-png 14 | ``` 15 | 16 | ## Usage 17 | The library provides two functions to add a new metadata: 18 | 19 | ```javascript 20 | addMetadata(PNGUint8Array, key, value) // stores the given key-value inside the PNG, provided as Uint8Array 21 | addMetadataFromBase64DataURI(dataURI, key, value) // stores the given key-value inside the PNG, provided as a Data URL string 22 | ``` 23 | 24 | and a function to get back the stored value: 25 | 26 | ```javascript 27 | getMetadata(PNGUint8Array, key) // retrives the given key from a PNG provided as Uint8Array 28 | ``` 29 | 30 | ## Limitations 31 | - tEXt chunks inside PNG files are meant to use the Latin-1 standard: using characters outside this charset may lead to unwanted behaviors 32 | - No check is performed inside add functions to test whether a given key is already present in the file. If you need it, you can call `getMetadata` beforehand and assert that it gives back `undefined`. 33 | -------------------------------------------------------------------------------- /types/main.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Adds in a tEXt chunk of a PNG file a metadata with the given key and value. 3 | * Warning: this function does not check whether the supplied key already exists. 4 | * 5 | * @param {Uint8Array} PNGUint8Array - Array containing bytes of a PNG file. 6 | * @param {string} key - Key (between 1 and 79 characters) used to identify the metadata. 7 | * @param {string} value - Value of the metadata to be set. 8 | * @returns {Uint8Array} - Array containing bytes of a PNG file with metadata. 9 | */ 10 | export function addMetadata(PNGUint8Array: Uint8Array, key: string, value: string): Uint8Array; 11 | /** 12 | * Adds in a tEXt chunk of a PNG file a metadata with the given key and value. 13 | * Warning: this function does not check whether the supplied key already exists. 14 | * 15 | * @param {string} dataURI - Data URL (staring with 'data:image/png;base64,') 16 | * containing a PNG file. 17 | * @param {string} key - Key (between 1 and 79 characters) used to identify the metadata. 18 | * @param {string} value - Value of the metadata to be set. 19 | * @returns {string} - Data URL with a base64 encoded PNG file with metadata. 20 | */ 21 | export function addMetadataFromBase64DataURI(dataURI: string, key: string, value: string): string; 22 | /** 23 | * Retrieves (if present) a metadata with the given key contained in a tEXt chunk 24 | * of a PNG file. 25 | * 26 | * @param {Uint8Array} PNGUint8Array - Array containing bytes of a PNG file. 27 | * @param {string} key - Key (between 1 and 79 characters) used to identify the metadata. 28 | * @returns {string|undefined} - A string containing the extracted value or undefined if it could 29 | * not be found. 30 | */ 31 | export function getMetadata(PNGUint8Array: Uint8Array, key: string): string | undefined; 32 | //# sourceMappingURL=main.d.ts.map -------------------------------------------------------------------------------- /.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 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 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 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs'); 2 | const exifr = require('exifr'); 3 | const { addMetadata } = require('../src/main'); 4 | const { addMetadataFromBase64DataURI } = require('../src/main'); 5 | const { getMetadata } = require('../src/main'); 6 | 7 | global.TextEncoder = require('util').TextEncoder; 8 | 9 | test('Stores simple data (from ArrayBuffer)', async () => { 10 | const PNGUint8Array = new Uint8Array(fs.readFileSync('./test/sample.png')); 11 | const PNGModified = addMetadata(PNGUint8Array, 'foo', 'bar'); 12 | const parsedTags = await exifr.parse(PNGModified); 13 | expect(parsedTags.foo).toBe('bar'); 14 | }); 15 | 16 | test('Stores simple data (from base64 Data URI)', async () => { 17 | const dataURI = fs.readFileSync('./test/samplePNGDataURI.txt', 'utf-8'); 18 | const modifiedDataURI = addMetadataFromBase64DataURI(dataURI, 'foo', 'bar'); 19 | const prefix = 'data:image/png;base64,'; 20 | expect(modifiedDataURI.startsWith(prefix)).toBe(true); 21 | const array = new Uint8Array(Buffer.from(modifiedDataURI.substring(prefix.length), 'base64')); 22 | const parsedTags = await exifr.parse(array); 23 | expect(parsedTags.foo).toBe('bar'); 24 | }); 25 | 26 | test('Rejects to write on invalid PNG', () => { 27 | const arrayBuffer = new Uint8Array(fs.readFileSync('./test/sample.jpg')); 28 | expect(() => addMetadata(arrayBuffer, 'foo', 'bar')).toThrow('Invalid PNG'); 29 | }); 30 | 31 | test('Rejects without crashing a small invalid PNG', () => { 32 | const arrayBuffer = new Uint8Array([0]); 33 | expect(() => addMetadata(arrayBuffer, 'foo', 'bar')).toThrow('Invalid PNG'); 34 | }); 35 | 36 | test('Rejects to write on invalid Data URI', () => { 37 | const dataURI = fs.readFileSync('./test/sampleGIFDataURI.txt', 'utf-8'); 38 | expect(() => addMetadataFromBase64DataURI(dataURI, 'foo', 'bar')).toThrow('Invalid PNG as Base64 Data URI'); 39 | }); 40 | 41 | test('Rejects to read from invalid PNG', () => { 42 | const arrayBuffer = new Uint8Array(fs.readFileSync('./test/sample.jpg')); 43 | expect(() => getMetadata(arrayBuffer, 'foo')).toThrow('Invalid PNG'); 44 | }); 45 | 46 | test('Rejects too short or too long key', () => { 47 | const arrayBuffer = new Uint8Array(fs.readFileSync('./test/sample.png')); 48 | const key = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'; 49 | expect(() => addMetadata(arrayBuffer, key, 'foo')).toThrow('Invalid length for key'); 50 | expect(() => addMetadata(arrayBuffer, '', 'foo')).toThrow('Invalid length for key'); 51 | }); 52 | 53 | test('Read simple data', () => { 54 | const PNGUint8Array = new Uint8Array(fs.readFileSync('./test/samplewithmeta.png')); 55 | const metadataValue = getMetadata(PNGUint8Array, 'foo'); 56 | expect(metadataValue).toBe('bar'); 57 | }); 58 | 59 | test('Read non-existent key', () => { 60 | const PNGUint8Array = new Uint8Array(fs.readFileSync('./test/samplewithmeta.png')); 61 | const metadataValue = getMetadata(PNGUint8Array, 'foofoo'); 62 | expect(metadataValue).toBe(undefined); 63 | }); 64 | 65 | test('Store and read short data', async () => { 66 | const PNGUint8Array = new Uint8Array(fs.readFileSync('./test/sample.png')); 67 | const key = 'pangram'; 68 | const value = 'The quick brown fox jumps over the lazy dog'; 69 | const PNGModified = addMetadata(PNGUint8Array, key, value); 70 | const parsedTags = await exifr.parse(PNGModified); 71 | expect(parsedTags[key]).toBe(value); 72 | const metadataValue = getMetadata(PNGModified, key); 73 | expect(metadataValue).toBe(value); 74 | }); 75 | 76 | test('Store and read long JSON data', async () => { 77 | const PNGUint8Array = new Uint8Array(fs.readFileSync('./test/sample.png')); 78 | const key = 'json'; 79 | let veryLongString = '1234567890'; 80 | for (let i = 0; i < 10; i += 1) { 81 | veryLongString += veryLongString + veryLongString; 82 | } 83 | // veryLongString is ~ 590kB 84 | const value = JSON.stringify({ foo: veryLongString }); 85 | const PNGModified = addMetadata(PNGUint8Array, key, value); 86 | const parsedTags = await exifr.parse(PNGModified); 87 | expect(parsedTags[key]).toBe(value); 88 | const metadataValue = getMetadata(PNGModified, key); 89 | expect(metadataValue).toBe(value); 90 | }); 91 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Packs a number into a 4 bytes Uint8Array, treating it as uint32. 3 | * 4 | * @param {number} n - The number to convert 5 | * @returns {Uint8Array} - The array 6 | */ 7 | function packNumberAs4Bytes(n) { 8 | const buffer = new ArrayBuffer(4); 9 | const view = new DataView(buffer); 10 | view.setUint32(0, n); 11 | return new Uint8Array(view.buffer); 12 | } 13 | 14 | /** 15 | * Return a string from a DataView, starting from a certain offset, up to length bytes. 16 | * 17 | * @param {DataView} view - The source DataView 18 | * @param {number=} offset - The offset from which to start reading the DataView 19 | * @param {number=} length - The maximum number of bytes to read 20 | * 21 | * @returns {string} - A string containing the interpetation of the values in the view. 22 | */ 23 | function dataViewToString(view, offset = 0, length = view.byteLength) { 24 | let res = ''; 25 | const maxLength = Math.min(length, view.byteLength - offset); 26 | for (let i = 0; i < maxLength; i += 1) { 27 | res += String.fromCharCode(view.getUint8(offset + i)); 28 | } 29 | return res; 30 | } 31 | 32 | /** 33 | * Encodes a string (UTF-8) into an array of bytes. 34 | * 35 | * @param {string} s - The string to encode 36 | * @returns {Uint8Array} - The resulting array containing the encoded string. 37 | */ 38 | function encodeString(s) { 39 | return new TextEncoder().encode(s); 40 | } 41 | 42 | /** 43 | * Concatenates (joins) a sequence of Uint8Arrays. 44 | * 45 | * @param {Uint8Array[]} arrays - An array of Uint8Arrays to be concatenated in order. 46 | * @returns {Uint8Array} - The resulting concatenated array. 47 | */ 48 | function concatArrays(arrays) { 49 | const length = arrays.reduce((prev, array) => prev + array.byteLength, 0); 50 | const finalArray = new Uint8Array(length); 51 | for (let i = 0, lenSoFar = 0; i < arrays.length; i += 1) { 52 | finalArray.set(arrays[i], lenSoFar); 53 | lenSoFar += arrays[i].byteLength; 54 | } 55 | return finalArray; 56 | } 57 | 58 | /** 59 | * Returns a dataview on the entire the TypedArray/Buffer. 60 | * We need to be very careful here, because the are subtle incompatibilities 61 | * between JavaScript TypedArrays and Node.js Buffers. 62 | * A Buffer might not start from a zero offset on the underlying ArrayBuffer. 63 | * Using byteOffset, even when `array` is using only a portion of the 64 | * underlying ArrayBuffer (e.g., for optimization purposes with small arrays), 65 | * we still get a view on the right values. 66 | * See https://nodejs.org/api/buffer.html#bufbyteoffset 67 | * @param {Uint8Array} array - The array to get a DataView on. 68 | * @returns {DataView} - A DataView on the array. 69 | */ 70 | function getDataView(array) { 71 | return new DataView(array.buffer, array.byteOffset, array.byteLength); 72 | } 73 | 74 | /** 75 | * Checks whether a given array has a valid PNG header. 76 | * 77 | * @param {Uint8Array} array - The array to check. 78 | * @returns {boolean} - true if array has a valid PNG header, false otherwise. 79 | */ 80 | function isPNG(array) { 81 | const pngSignature = new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]); 82 | if (array.length < pngSignature.length) { 83 | return false; 84 | } 85 | return getDataView(pngSignature).getBigUint64() === getDataView(array).getBigUint64(); 86 | } 87 | 88 | /** 89 | * Computes the CRC32 of a given array. 90 | * Source: https://github.com/image-js/fast-png/blob/bdb81f93cc55aa89b312b50e5e2e8a39cdbde657/src/common.ts 91 | * 92 | * @param {Uint8Array} data - The array to compute the CRC32 of. 93 | * @returns {number} - The CRC32 of the array. 94 | */ 95 | 96 | const crcTable = []; 97 | function crc(data) { 98 | /* eslint-disable no-plusplus */ 99 | /* eslint-disable no-bitwise */ 100 | // Pre-compute CRC table 101 | if (crcTable.length === 0) { 102 | for (let n = 0; n < 256; n++) { 103 | let c = n; 104 | for (let k = 0; k < 8; k++) { 105 | if (c & 1) { 106 | c = 0xedb88320 ^ (c >>> 1); 107 | } else { 108 | c >>>= 1; 109 | } 110 | } 111 | crcTable[n] = c; 112 | } 113 | } 114 | const initialCrc = 0xffffffff; 115 | const updateCrc = (currentCrc, d, length) => { 116 | let c = currentCrc; 117 | for (let n = 0; n < length; n++) { 118 | c = crcTable[(c ^ d[n]) & 0xff] ^ (c >>> 8); 119 | } 120 | return c; 121 | }; 122 | return (updateCrc(initialCrc, data, data.byteLength) ^ initialCrc) >>> 0; 123 | } 124 | 125 | /** 126 | * Adds in a tEXt chunk of a PNG file a metadata with the given key and value. 127 | * Warning: this function does not check whether the supplied key already exists. 128 | * 129 | * @param {Uint8Array} PNGUint8Array - Array containing bytes of a PNG file. 130 | * @param {string} key - Key (between 1 and 79 characters) used to identify the metadata. 131 | * @param {string} value - Value of the metadata to be set. 132 | * @returns {Uint8Array} - Array containing bytes of a PNG file with metadata. 133 | */ 134 | export function addMetadata(PNGUint8Array, key, value) { 135 | if (!isPNG(PNGUint8Array)) { 136 | throw new TypeError('Invalid PNG'); 137 | } 138 | 139 | if (key.length < 1 || key.length > 79) { 140 | throw new TypeError('Invalid length for key'); 141 | } 142 | 143 | // Prepare tEXt chunk to insert 144 | const chunkType = encodeString('tEXt'); 145 | const chunkData = encodeString(`${key}\0${value}`); 146 | const chunkCRC = packNumberAs4Bytes(crc(concatArrays([chunkType, chunkData]))); 147 | const chunkDataLen = packNumberAs4Bytes(chunkData.byteLength); 148 | const chunk = concatArrays([chunkDataLen, chunkType, chunkData, chunkCRC]); 149 | 150 | // Compute header (IHDR) length 151 | const headerDataLenOffset = 8; 152 | const headerDataLen = getDataView(PNGUint8Array).getUint32(headerDataLenOffset); 153 | const headerLen = 8 + 4 + 4 + headerDataLen + 4; 154 | 155 | // Assemble new PNG 156 | const head = PNGUint8Array.subarray(0, headerLen); 157 | const tail = PNGUint8Array.subarray(headerLen); 158 | return concatArrays([head, chunk, tail]); 159 | } 160 | 161 | /** 162 | * Adds in a tEXt chunk of a PNG file a metadata with the given key and value. 163 | * Warning: this function does not check whether the supplied key already exists. 164 | * 165 | * @param {string} dataURI - Data URL (staring with 'data:image/png;base64,') 166 | * containing a PNG file. 167 | * @param {string} key - Key (between 1 and 79 characters) used to identify the metadata. 168 | * @param {string} value - Value of the metadata to be set. 169 | * @returns {string} - Data URL with a base64 encoded PNG file with metadata. 170 | */ 171 | export function addMetadataFromBase64DataURI(dataURI, key, value) { 172 | const prefix = 'data:image/png;base64,'; 173 | if (typeof dataURI !== 'string' || dataURI.substring(0, prefix.length) !== prefix) { 174 | throw new TypeError('Invalid PNG as Base64 Data URI'); 175 | } 176 | const dataStr = atob(dataURI.substring(prefix.length)); 177 | const PNGUint8Array = new Uint8Array(dataStr.length); 178 | for (let i = 0; i < dataStr.length; i += 1) { 179 | PNGUint8Array[i] = dataStr.charCodeAt(i); 180 | } 181 | const newPNGUint8Array = addMetadata(PNGUint8Array, key, value); 182 | return prefix + btoa(dataViewToString(getDataView(newPNGUint8Array))); 183 | } 184 | 185 | /** 186 | * Retrieves (if present) a metadata with the given key contained in a tEXt chunk 187 | * of a PNG file. 188 | * 189 | * @param {Uint8Array} PNGUint8Array - Array containing bytes of a PNG file. 190 | * @param {string} key - Key (between 1 and 79 characters) used to identify the metadata. 191 | * @returns {string|undefined} - A string containing the extracted value or undefined if it could 192 | * not be found. 193 | */ 194 | export function getMetadata(PNGUint8Array, key) { 195 | if (!isPNG(PNGUint8Array)) { 196 | throw new TypeError('Invalid PNG'); 197 | } 198 | 199 | const view = getDataView(PNGUint8Array); 200 | let offset = 8; 201 | while (offset < view.byteLength) { 202 | const chunkLength = view.getUint32(offset); 203 | if (dataViewToString(view, offset + 4, 4 + key.length) === `tEXt${key}`) { 204 | return dataViewToString(view, offset + 4 + 4 + key.length + 1, chunkLength - key.length - 1); 205 | } 206 | offset += chunkLength + 12; // skip 4 bytes of chunkLength, 4 of chunkType, 4 of CRC 207 | } 208 | return undefined; 209 | } 210 | --------------------------------------------------------------------------------