remove EXIF information from PNG, JPEG and WebP images jsr.io/@mary/exif-rm
typescript jsr
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

initial commit

Mary 7f005602

+178
+3
.vscode/settings.json
··· 1 + { 2 + "editor.defaultFormatter": "denoland.vscode-deno" 3 + }
+17
LICENSE
··· 1 + Permission is hereby granted, free of charge, to any person obtaining a copy 2 + of this software and associated documentation files (the "Software"), to deal 3 + in the Software without restriction, including without limitation the rights 4 + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 5 + copies of the Software, and to permit persons to whom the Software is 6 + furnished to do so, subject to the following conditions: 7 + 8 + The above copyright notice and this permission notice shall be included in all 9 + copies or substantial portions of the Software. 10 + 11 + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 12 + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 13 + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 14 + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 15 + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 16 + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 17 + SOFTWARE.
+8
README.md
··· 1 + # exif-rm 2 + 3 + Remove EXIF information from PNG and JPEG images. 4 + 5 + ```ts 6 + const image = await Deno.readFile('./samples/sample.jpg'); 7 + const exifRemoved = remove(image.buffer); 8 + ```
+15
deno.json
··· 1 + { 2 + "name": "@mary/exif-rm", 3 + "version": "0.1.0", 4 + "exports": "./mod.ts", 5 + "fmt": { 6 + "useTabs": true, 7 + "indentWidth": 2, 8 + "lineWidth": 110, 9 + "semiColons": true, 10 + "singleQuote": true 11 + }, 12 + "publish": { 13 + "exclude": [".vscode/", "samples/", "deno.lock", "test.ts"] 14 + } 15 + }
+21
deno.lock
··· 1 + { 2 + "version": "3", 3 + "packages": { 4 + "specifiers": { 5 + "jsr:@std/assert": "jsr:@std/assert@0.218.2", 6 + "jsr:@std/fmt@^0.218.2": "jsr:@std/fmt@0.218.2" 7 + }, 8 + "jsr": { 9 + "@std/assert@0.218.2": { 10 + "integrity": "7f0a5a1a8cf86607cd6c2c030584096e1ffad27fc9271429a8cb48cfbdee5eaf", 11 + "dependencies": [ 12 + "jsr:@std/fmt@^0.218.2" 13 + ] 14 + }, 15 + "@std/fmt@0.218.2": { 16 + "integrity": "99526449d2505aa758b6cbef81e7dd471d8b28ec0dcb1491d122b284c548788a" 17 + } 18 + } 19 + }, 20 + "remote": {} 21 + }
+95
mod.ts
··· 1 + /** 2 + * Removes EXIF information from PNG and JPEG images 3 + * @returns A new image buffer with EXIF removed, will return `null` if 4 + * image is unsupported, or if there's nothing to remove. 5 + */ 6 + export const remove = (buf: ArrayBuffer): Uint8Array | null => { 7 + const view = new DataView(buf); 8 + const indices: [start: number, end: number][] = []; 9 + 10 + const blen = view.byteLength; 11 + 12 + let start = 0; 13 + 14 + if (blen >= 2 && view.getUint16(0) === 0xffd8) { 15 + // JPEG 16 + let pos = 2; 17 + 18 + while (pos + 4 + 5 <= blen) { 19 + const marker = view.getUint16(pos); 20 + 21 + if (/* Fill */ marker === 0xffff) { 22 + pos++; 23 + 24 + continue; 25 + } else if ( 26 + /* App0..App15 */ (marker >= 0xffe0 && marker <= 0xffef) || 27 + /* Comment */ marker === 0xfffe || 28 + /* SOF0 */ marker === 0xffc0 || 29 + /* SOF2 */ marker === 0xffc2 || 30 + /* DHT */ marker === 0xffc4 || 31 + /* DQT */ marker === 0xffdb || 32 + /* DRI */ marker === 0xffdd || 33 + /* SOS */ marker === 0xffda 34 + ) { 35 + const flen = view.getUint16(pos + 2); 36 + const end = pos + 2 + flen; 37 + 38 + if ( 39 + /* App1 */ marker === 0xffe1 && 40 + /* Exif */ view.getUint32(pos + 4) === 0x45786966 && 41 + /* null */ view.getUint16(pos + 8) === 0x0 42 + ) { 43 + indices.push([start, pos]); 44 + start = end; 45 + } 46 + 47 + pos = end; 48 + continue; 49 + } 50 + 51 + break; 52 + } 53 + } else if (blen >= 8 && view.getUint32(0) === 0x89504e47 && view.getUint32(4) === 0x0d0a1a0a) { 54 + // PNG 55 + let pos = 8; 56 + 57 + while (pos + 4 + 4 <= blen) { 58 + const flen = view.getUint32(pos); 59 + const marker = view.getUint32(pos + 4); 60 + 61 + const end = pos + flen + 4 + 4 + 4; 62 + 63 + if ( 64 + /* eXIf */ marker === 0x65584966 || 65 + /* tIME */ marker === 0x74494d45 || 66 + /* iTXt */ marker === 0x69545874 || 67 + /* tEXt */ marker === 0x74455874 || 68 + /* zTXT */ marker === 0x7a545874 || 69 + /* dSIG */ marker === 0x64534947 70 + ) { 71 + indices.push([start, pos]); 72 + start = end; 73 + } 74 + 75 + pos = end; 76 + } 77 + } 78 + 79 + if (start === 0) { 80 + return null; 81 + } 82 + 83 + const uint8 = new Uint8Array(buf); 84 + const copy = new Uint8Array(indices.reduce((accu, index) => accu + (index[1] - index[0]), blen - start)); 85 + 86 + copy.set( 87 + uint8.subarray(start), 88 + indices.reduce((offset, index) => { 89 + copy.set(uint8.subarray(index[0], index[1]), offset); 90 + return offset + (index[1] - index[0]); 91 + }, 0), 92 + ); 93 + 94 + return copy; 95 + };
samples/sample.jpg

This is a binary file and will not be displayed.

samples/sample.png

This is a binary file and will not be displayed.

+19
test.ts
··· 1 + import { assert } from 'jsr:@std/assert'; 2 + 3 + import { remove } from './mod.ts'; 4 + 5 + Deno.test('removes EXIF from JPEG files', async () => { 6 + const image = await Deno.readFile('./samples/sample.jpg'); 7 + const exifRemoved = remove(image.buffer); 8 + 9 + assert(exifRemoved != null); 10 + assert(exifRemoved.byteLength === 5480); 11 + }); 12 + 13 + Deno.test('removes EXIF from PNG files', async () => { 14 + const image = await Deno.readFile('./samples/sample.png'); 15 + const exifRemoved = remove(image.buffer); 16 + 17 + assert(exifRemoved != null); 18 + assert(exifRemoved.byteLength === 17638); 19 + });