<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Amjad Khan</title>
    <description>The latest articles on DEV Community by Amjad Khan (@amjad_khan_3752d036cd1d58).</description>
    <link>https://dev.to/amjad_khan_3752d036cd1d58</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1941172%2F21d23253-1354-4f51-a220-571c7b71523d.png</url>
      <title>DEV Community: Amjad Khan</title>
      <link>https://dev.to/amjad_khan_3752d036cd1d58</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/amjad_khan_3752d036cd1d58"/>
    <language>en</language>
    <item>
      <title>Stripping EXIF and GPS from photos in the browser, losslessly, in ~500 lines of vanilla JS</title>
      <dc:creator>Amjad Khan</dc:creator>
      <pubDate>Thu, 15 Oct 2026 10:22:49 +0000</pubDate>
      <link>https://dev.to/amjad_khan_3752d036cd1d58/stripping-exif-and-gps-from-photos-in-the-browser-losslessly-in-500-lines-of-vanilla-js-ll4</link>
      <guid>https://dev.to/amjad_khan_3752d036cd1d58/stripping-exif-and-gps-from-photos-in-the-browser-losslessly-in-500-lines-of-vanilla-js-ll4</guid>
      <description>&lt;p&gt;Photos from phones carry more than pixels. Open the file and there's often the exact GPS position it was taken at, the phone model, lens and settings, the date and time, sometimes the owner's name or a camera serial number, and, increasingly, labels saying whether an image was edited with generative AI.&lt;/p&gt;

&lt;p&gt;I make a small Android camera app, and people kept asking whether the photos they share give away where they live. So I built a checker that shows what a photo file carries and saves a clean copy, without uploading anything:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://blinqdroid.com/tools/photo-privacy-check/" rel="noopener noreferrer"&gt;https://blinqdroid.com/tools/photo-privacy-check/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No libraries, no server, no WebAssembly: a &lt;code&gt;DataView&lt;/code&gt; over the file's bytes. Here's how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading the file without uploading it
&lt;/h2&gt;

&lt;p&gt;The whole thing starts with the bytes in memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;u8&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there it's container walking. The first bytes tell you the format: &lt;code&gt;FF D8&lt;/code&gt; is a JPEG, &lt;code&gt;89 PNG&lt;/code&gt; is a PNG, &lt;code&gt;RIFF....WEBP&lt;/code&gt; is a WebP, and &lt;code&gt;II&lt;/code&gt;/&lt;code&gt;MM&lt;/code&gt; at the start is a TIFF (which is also what a DNG raw file is).&lt;/p&gt;

&lt;h2&gt;
  
  
  JPEG: walking the markers
&lt;/h2&gt;

&lt;p&gt;A JPEG is a list of segments. Each starts with &lt;code&gt;0xFF&lt;/code&gt;, a marker byte, then a two-byte big-endian length:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// skip SOI (FF D8)&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mh"&gt;0xFF&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nx"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;len&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mh"&gt;0xE1&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nf"&gt;ascii&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Exif&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;readTiff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// APP1 can also hold XMP: "http://ns.adobe.com/xap/1.0/\0" + XML&lt;/span&gt;
  &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;start&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fiddly part is the image data itself. After a Start of Scan (&lt;code&gt;FF DA&lt;/code&gt;) segment comes entropy-coded data with no length field. Inside it, a literal &lt;code&gt;0xFF&lt;/code&gt; byte is "stuffed" as &lt;code&gt;FF 00&lt;/code&gt;, and restart markers &lt;code&gt;FF D0&lt;/code&gt;–&lt;code&gt;FF D7&lt;/code&gt; can appear. So to find the next real segment (progressive JPEGs have several scans, with tables in between) you scan forward for an &lt;code&gt;0xFF&lt;/code&gt; that is followed by something other than &lt;code&gt;00&lt;/code&gt; or a restart marker.&lt;/p&gt;

&lt;h2&gt;
  
  
  EXIF is a tiny TIFF file
&lt;/h2&gt;

&lt;p&gt;The EXIF block is a TIFF structure: a byte-order mark (&lt;code&gt;II&lt;/code&gt; little-endian, &lt;code&gt;MM&lt;/code&gt; big-endian), the number 42, and an offset to the first IFD (image file directory). Each IFD entry is 12 bytes: tag, type, count, and either the value or an offset to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;le&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ascii&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;II&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataView&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;u8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;byteOffset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ifdOffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;le&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;ifdOffset&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;le&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getUint16&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;le&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// 0x8769 points to the Exif sub-IFD, 0x8825 to the GPS IFD&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GPS coordinates are three rationals (degrees, minutes, seconds), each a pair of 32-bit integers, plus a reference letter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;dms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;SW&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The IFD after the first one (IFD1) is the embedded thumbnail. It's easy to forget, and it can still show the uncropped original after you've cropped the photo.&lt;/p&gt;

&lt;h2&gt;
  
  
  PNG, WebP and the AI labels
&lt;/h2&gt;

&lt;p&gt;PNG is a list of chunks (&lt;code&gt;length&lt;/code&gt;, &lt;code&gt;type&lt;/code&gt;, &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;CRC&lt;/code&gt;). Metadata lives in &lt;code&gt;eXIf&lt;/code&gt; (the same TIFF structure), &lt;code&gt;tEXt&lt;/code&gt;/&lt;code&gt;iTXt&lt;/code&gt;/&lt;code&gt;zTXt&lt;/code&gt; text chunks and &lt;code&gt;tIME&lt;/code&gt;. Some image generators write their whole prompt into a &lt;code&gt;tEXt&lt;/code&gt; chunk called &lt;code&gt;parameters&lt;/code&gt;, so the tool shows text chunks verbatim.&lt;/p&gt;

&lt;p&gt;WebP is RIFF: four-character chunk IDs with little-endian sizes, padded to even lengths. &lt;code&gt;EXIF&lt;/code&gt; and &lt;code&gt;XMP&lt;/code&gt; are separate chunks, and the &lt;code&gt;VP8X&lt;/code&gt; header has flag bits saying they're present.&lt;/p&gt;

&lt;p&gt;In XMP, the interesting fields for 2026 are the IPTC &lt;code&gt;DigitalSourceType&lt;/code&gt; values (&lt;code&gt;trainedAlgorithmicMedia&lt;/code&gt; means made with generative AI, &lt;code&gt;compositeWithTrainedAlgorithmicMedia&lt;/code&gt; edited with it) and whether a Content Credentials (C2PA) manifest is embedded. The tool only detects C2PA; it doesn't validate the signature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning without re-compressing
&lt;/h2&gt;

&lt;p&gt;The easy way to strip metadata is to draw the image on a canvas and export it. That re-encodes the JPEG and loses quality. Instead, for JPEG, PNG and WebP the tool rebuilds the file from the parts it keeps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JPEG:&lt;/strong&gt; keep everything except &lt;code&gt;APP1&lt;/code&gt;–&lt;code&gt;APP15&lt;/code&gt; and &lt;code&gt;COM&lt;/code&gt; segments, with two exceptions: the ICC colour profile (&lt;code&gt;APP2 "ICC_PROFILE"&lt;/code&gt;) and Adobe's &lt;code&gt;APP14&lt;/code&gt;, because dropping those changes how colours display. The image data is copied byte for byte.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PNG:&lt;/strong&gt; drop &lt;code&gt;eXIf&lt;/code&gt;, &lt;code&gt;tEXt&lt;/code&gt;, &lt;code&gt;iTXt&lt;/code&gt;, &lt;code&gt;zTXt&lt;/code&gt;, &lt;code&gt;tIME&lt;/code&gt; and the C2PA &lt;code&gt;caBX&lt;/code&gt; chunk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebP:&lt;/strong&gt; drop &lt;code&gt;EXIF&lt;/code&gt; and &lt;code&gt;XMP&lt;/code&gt; chunks, clear their bits in &lt;code&gt;VP8X&lt;/code&gt;, and rewrite the RIFF size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One catch with JPEGs: phones often store the image sideways and add an Orientation tag saying how to display it. Strip all EXIF and the photo shows up rotated. So the clean JPEG gets a brand-new 36-byte EXIF segment containing exactly one tag, Orientation, and nothing else.&lt;/p&gt;

&lt;p&gt;Other formats the browser can decode (AVIF, for example) fall back to a canvas re-encode as JPEG at quality 0.95. DNG is read-only, and HEIC can only be cleaned in browsers that can decode it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving it worked
&lt;/h2&gt;

&lt;p&gt;Before offering the download, the page runs the same parser on the clean copy and reports anything left. It's a small step, but it turns "trust me" into "here's what the output contains":&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;again&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;analyse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;())));&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;left&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;location&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;identity&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;device&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;time&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;software&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ai&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;again&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;k&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Privacy, verifiably
&lt;/h2&gt;

&lt;p&gt;The page runs under a strict Content Security Policy (no inline scripts, no third-party scripts), and the photo never leaves the tab: load the page, switch off your network, and it still works. The script isn't minified, so you can read it at &lt;code&gt;/js/photo-check.js&lt;/code&gt;. The only request it makes is to my own page counter, saying that a photo was checked or cleaned. There are no cookies and no photo data in it.&lt;/p&gt;

&lt;p&gt;If you find a file it misreads, I'd love to hear about it in the comments: the metadata, not the photo, or a test file with fake details.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>privacy</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
