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.

chore: use imagemagick to verify exif data

Mary c7a9ece4 49d38ed4

+141 -5
+141 -5
test.ts
··· 1 - import { assert } from 'jsr:@std/assert'; 1 + import { assert, assertEquals } from 'jsr:@std/assert'; 2 2 3 3 import { remove } from './mod.ts'; 4 4 5 + const identify = (() => { 6 + const decoder = new TextDecoder('utf-8'); 7 + const command = new Deno.Command('identify', { 8 + args: ['-regard-warnings', '-format', '%[EXIF:*]', '-'], 9 + stdin: 'piped', 10 + stdout: 'piped', 11 + stderr: 'piped', 12 + }); 13 + 14 + return async (image: Uint8Array) => { 15 + const process = command.spawn(); 16 + 17 + { 18 + const writer = process.stdin.getWriter(); 19 + await writer.write(image); 20 + await writer.close(); 21 + } 22 + 23 + const { code, stdout, stderr } = await process.output(); 24 + if (code !== 0) { 25 + throw new Error(`exit code ${code}\n${decoder.decode(stderr)}`); 26 + } 27 + 28 + const text = decoder.decode(stdout); 29 + const lines = text.split('\n').filter((line) => line.length > 0); 30 + 31 + const fields = lines.map((line) => { 32 + const [key, ...rest] = line.split('='); 33 + const value = rest.join('='); 34 + 35 + return [key, value]; 36 + }); 37 + 38 + return Object.fromEntries(fields); 39 + }; 40 + })(); 41 + 5 42 Deno.test('removes EXIF from JPEG files', async () => { 6 43 const image = await Deno.readFile('./samples/sample.jpg'); 44 + 45 + // Verify the original image has EXIF data 46 + assertEquals(await identify(image), { 47 + 'exif:ApertureValue': '368640/65536', 48 + 'exif:ColorSpace': '1', 49 + 'exif:ComponentsConfiguration': '...', 50 + 'exif:CustomRendered': '0', 51 + 'exif:DateTime': '2008:07:31 10:38:11', 52 + 'exif:DateTimeDigitized': '2008:05:30 15:56:01', 53 + 'exif:DateTimeOriginal': '2008:05:30 15:56:01', 54 + 'exif:ExifOffset': '214', 55 + 'exif:ExifVersion': '0221', 56 + 'exif:ExposureBiasValue': '0/1', 57 + 'exif:ExposureMode': '1', 58 + 'exif:ExposureProgram': '1', 59 + 'exif:ExposureTime': '1/160', 60 + 'exif:FNumber': '71/10', 61 + 'exif:Flash': '9', 62 + 'exif:FlashPixVersion': '0100', 63 + 'exif:FocalLength': '135/1', 64 + 'exif:FocalPlaneResolutionUnit': '2', 65 + 'exif:FocalPlaneXResolution': '3888000/876', 66 + 'exif:FocalPlaneYResolution': '2592000/583', 67 + 'exif:GPSInfo': '978', 68 + 'exif:GPSVersionID': '....', 69 + 'exif:InteroperabilityOffset': '948', 70 + 'exif:Make': 'Canon', 71 + 'exif:MeteringMode': '5', 72 + 'exif:Model': 'Canon EOS 40D', 73 + 'exif:Orientation': '1', 74 + 'exif:PhotographicSensitivity': '100', 75 + 'exif:PixelXDimension': '100', 76 + 'exif:PixelYDimension': '68', 77 + 'exif:ResolutionUnit': '2', 78 + 'exif:SceneCaptureType': '0', 79 + 'exif:ShutterSpeedValue': '483328/65536', 80 + 'exif:Software': 'GIMP 2.4.5', 81 + 'exif:SubSecTime': '00', 82 + 'exif:SubSecTimeDigitized': '00', 83 + 'exif:SubSecTimeOriginal': '00', 84 + 'exif:UserComment': '', 85 + 'exif:WhiteBalance': '0', 86 + 'exif:XResolution': '72/1', 87 + 'exif:YCbCrPositioning': '2', 88 + 'exif:YResolution': '72/1', 89 + 'exif:thumbnail:Compression': '6', 90 + 'exif:thumbnail:InteroperabilityIndex': 'R98', 91 + 'exif:thumbnail:InteroperabilityVersion': '0100', 92 + 'exif:thumbnail:JPEGInterchangeFormat': '1090', 93 + 'exif:thumbnail:JPEGInterchangeFormatLength': '1378', 94 + 'exif:thumbnail:ResolutionUnit': '2', 95 + 'exif:thumbnail:XResolution': '72/1', 96 + 'exif:thumbnail:YResolution': '72/1', 97 + }); 98 + 7 99 const exifRemoved = remove(image); 8 100 9 - assert(exifRemoved != null); 10 - assert(exifRemoved.byteLength === 5480); 101 + // Verify the image has no EXIF data 102 + assert(exifRemoved !== null); 103 + assertEquals(await identify(exifRemoved), {}); 11 104 }); 12 105 13 106 Deno.test('removes EXIF from PNG files', async () => { 14 107 const image = await Deno.readFile('./samples/sample.png'); 108 + 109 + // Verify the original image has EXIF data 110 + assertEquals(await identify(image), { 111 + 'exif:ApertureValue': '45/8', 112 + 'exif:ColorSpace': '1', 113 + 'exif:ComponentsConfiguration': '...', 114 + 'exif:CustomRendered': '0', 115 + 'exif:DateTime': '2008:07:31 10:38:11', 116 + 'exif:DateTimeDigitized': '2008:05:30 15:56:01', 117 + 'exif:DateTimeOriginal': '2008:05:30 15:56:01', 118 + 'exif:ExifOffset': '190', 119 + 'exif:ExifVersion': '0221', 120 + 'exif:ExposureBiasValue': '0/1', 121 + 'exif:ExposureMode': '1', 122 + 'exif:ExposureProgram': '1', 123 + 'exif:ExposureTime': '1/160', 124 + 'exif:FNumber': '71/10', 125 + 'exif:Flash': '9', 126 + 'exif:FlashPixVersion': '0100', 127 + 'exif:FocalLength': '135/1', 128 + 'exif:FocalPlaneResolutionUnit': '2', 129 + 'exif:FocalPlaneXResolution': '324000/73', 130 + 'exif:FocalPlaneYResolution': '2592000/583', 131 + 'exif:Make': 'Canon', 132 + 'exif:MeteringMode': '5', 133 + 'exif:Model': 'Canon EOS 40D', 134 + 'exif:Orientation': '1', 135 + 'exif:PhotographicSensitivity': '100', 136 + 'exif:PixelXDimension': '100', 137 + 'exif:PixelYDimension': '68', 138 + 'exif:ResolutionUnit': '2', 139 + 'exif:SceneCaptureType': '0', 140 + 'exif:ShutterSpeedValue': '59/8', 141 + 'exif:Software': 'GIMP 2.4.5', 142 + 'exif:SubSecTime': '00', 143 + 'exif:SubSecTimeDigitized': '00', 144 + 'exif:SubSecTimeOriginal': '00', 145 + 'exif:WhiteBalance': '0', 146 + 'exif:XResolution': '72/1', 147 + 'exif:YResolution': '72/1', 148 + }); 149 + 15 150 const exifRemoved = remove(image); 16 151 17 - assert(exifRemoved != null); 18 - assert(exifRemoved.byteLength === 17638); 152 + // Verify the image has no EXIF data 153 + assert(exifRemoved !== null); 154 + assertEquals(await identify(exifRemoved), {}); 19 155 });