<?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: Danilo Fortunato</title>
    <description>The latest articles on DEV Community by Danilo Fortunato (@danyblitz).</description>
    <link>https://dev.to/danyblitz</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%2F4155561%2Fee8cdfdc-ddf8-4380-b85b-0b95c62eb446.png</url>
      <title>DEV Community: Danilo Fortunato</title>
      <link>https://dev.to/danyblitz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/danyblitz"/>
    <language>en</language>
    <item>
      <title>The distribution checklist nobody writes down</title>
      <dc:creator>Danilo Fortunato</dc:creator>
      <pubDate>Sat, 10 Oct 2026 07:09:48 +0000</pubDate>
      <link>https://dev.to/danyblitz/the-distribution-checklist-nobody-writes-down-mj2</link>
      <guid>https://dev.to/danyblitz/the-distribution-checklist-nobody-writes-down-mj2</guid>
      <description>&lt;p&gt;I ship four small Python CLI tools. Not libraries — command-line utilities you install and run once. And I learned the hard way that building them is the easy part. The part with all the friction is putting them where someone who needs them will actually find them.&lt;/p&gt;

&lt;p&gt;This is the checklist I wish I had, with the exact walls I hit. No theory.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. PyPI name squatting is the first wall
&lt;/h2&gt;

&lt;p&gt;My website auditor is called SiteAuditor. The PyPI name &lt;code&gt;site-auditor&lt;/code&gt; was already taken by an unrelated project. So the distribution name and the command name had to be split:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;package:  danyblitz-site-auditor
command:  siteauditor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check the name before you fall in love with it. &lt;code&gt;pip index versions &amp;lt;name&amp;gt;&lt;/code&gt; or the PyPI JSON API tells you in one second whether it is free.&lt;/li&gt;
&lt;li&gt;Decide the &lt;strong&gt;package name&lt;/strong&gt; and the &lt;strong&gt;console-script name&lt;/strong&gt; separately. You are not forced to make them agree.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And drop the dash in the command name. &lt;code&gt;photo-metaclean&lt;/code&gt; as a package, &lt;code&gt;photometaclean&lt;/code&gt; as the command. A dash in a command name is friction in every doc, every alias, every shell one-liner.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. winget needs a real release asset, not a source zip
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;winget&lt;/code&gt; installs from a URL plus a SHA256. If your GitHub release has no &lt;code&gt;.exe&lt;/code&gt;, you cannot ship a winget package at all. The manifest is a &lt;code&gt;portable&lt;/code&gt; one, schema &lt;code&gt;1.6.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two frictions worth knowing before you start:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A one-time Microsoft CLA. The bot asks the account to reply &lt;code&gt;@microsoft-github-policy-service agree&lt;/code&gt;. It is a checkbox, but nothing moves until it is done.&lt;/li&gt;
&lt;li&gt;The review queue. Free, human, and unschedulable. Set expectations.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Directories are a queue, not a launch
&lt;/h2&gt;

&lt;p&gt;SaaSHub, AlternativeTo and friends: submission is free, review is slow, and it is a queue shared with everyone else. Some offer a paid "priority review"; the free path is fine if you stop expecting same-day visibility.&lt;/p&gt;

&lt;p&gt;The field that actually matters is &lt;strong&gt;alternatives&lt;/strong&gt;. A tool with no alternatives listed is nearly invisible. Add two or three real ones and you show up where people compare tools by function.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. The boring thing that mattered most
&lt;/h2&gt;

&lt;p&gt;The highest-leverage step was the least exciting: getting the tools into the package managers. A tool that is &lt;code&gt;pip install &amp;lt;name&amp;gt;&lt;/code&gt; away is discoverable in a way a landing page is not. My landing pages were fine for months and nobody came. The moment the packages were installable, the problem changed from "marketing" to "is the name right".&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-line version
&lt;/h2&gt;

&lt;p&gt;Publication is not discovery. A landing page pushes; it does not pull. Discovery lives in two places only:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;package managers (PyPI, winget, npm, Homebrew)&lt;/li&gt;
&lt;li&gt;directories where someone searches by what a tool does&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Everything else — Product Hunt, a dev.to post, a tweet — is a one-day spike on top of that base.&lt;/p&gt;




&lt;p&gt;If you want to see a finished tool of this kind: &lt;a href="https://github.com/danyblitz-bit/site-auditor" rel="noopener noreferrer"&gt;SiteAuditor&lt;/a&gt; audits a page and its links, &lt;a href="https://github.com/danyblitz-bit/photo-metaclean" rel="noopener noreferrer"&gt;Photo MetaClean&lt;/a&gt; strips EXIF, and both are &lt;code&gt;pip install&lt;/code&gt; away.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>cli</category>
      <category>github</category>
    </item>
    <item>
      <title>I ran my own website auditor on my own landing page. It gave me 89/100, and three of the five findings are noise</title>
      <dc:creator>Danilo Fortunato</dc:creator>
      <pubDate>Tue, 06 Oct 2026 06:42:27 +0000</pubDate>
      <link>https://dev.to/danyblitz/i-ran-my-own-website-auditor-on-my-own-landing-page-it-gave-me-89100-and-three-of-the-five-1efk</link>
      <guid>https://dev.to/danyblitz/i-ran-my-own-website-auditor-on-my-own-landing-page-it-gave-me-89100-and-three-of-the-five-1efk</guid>
      <description>&lt;p&gt;I ran my own website auditor on my own landing page. That is the only honest test a linter-like tool has: either it finds things you would rather it had not, or it is too generous to be trusted.&lt;/p&gt;

&lt;p&gt;Here is what it gave me.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  SiteAudit Report
  https://danyblitz-bit.github.io/site-auditor/

  Score: 89/100 (Grade B)
  Status: 200  |  Time: 524ms  |  Size: 5 KB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five warnings came back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;!  Title too long (67 chars)
!  Meta description too long (168 chars)
!  Missing Open Graph tags: og:title, og:description, og:image
!  No canonical link
!  Missing Content-Security-Policy header
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five findings, five equal markers, one score. The problem is that they are not equal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three of these are noise on this page
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Title too long (67 chars).&lt;/strong&gt; Google renders roughly 580px of it, which lands around 60 characters. So it will be truncated. Truncated is not broken — it still ranks, it still reads, it just gets cut. A real finding with a severity of almost nothing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Meta description too long (168 chars).&lt;/strong&gt; Same story, cut around 155. The meta description does not affect ranking at all; it affects whether someone clicks. Losing the last 13 characters of a 168-character description changes nothing measurable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No canonical link.&lt;/strong&gt; A canonical exists to tell search engines which of several duplicates is the real one. This is a single static page with one URL, served from GitHub Pages. There is no duplicate to point away from. The rule my own tool enforces is a rule that resolves a problem this page cannot have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two of them are not noise
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Missing Open Graph tags.&lt;/strong&gt; This is the one that actually bites, and it bites at the exact moment the product is being shared. When someone drops the link into Product Hunt, Slack, Twitter or a message, the preview falls back to whatever it can scrape: a title and, if you are lucky, an image. On a link you spent a week preparing, the preview is half the impression. This one is not a style point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Missing Content-Security-Policy.&lt;/strong&gt; The header is a real defence, not a metadata nicety: without it, a stray script that ends up on the page can run and exfiltrate. For a static one-page site with zero JavaScript, the practical risk is near zero — which is precisely why a score is the wrong instrument here. The same warning would be the most important line in the output for a page that logs users in.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a single number cannot express
&lt;/h2&gt;

&lt;p&gt;That is the part worth stealing from this. A score compresses twenty signals into one integer, and weighting is the only thing that separates an audit from a checklist. A report that flags five items as &lt;code&gt;!&lt;/code&gt; and tells you nothing about which of them is worth an hour of your time has handed you a todo list, not an opinion.&lt;/p&gt;

&lt;p&gt;The fix is not a cleverer formula — weights are context-dependent and anyone who hardcodes "canonical: 10 points" is guessing. The fix is that anything you automate for an end user has to end with a ranking and a reason, or the user has to do the ranking themselves, which is the work they were trying to avoid.&lt;/p&gt;

&lt;p&gt;In my case the honest answer for this page is: add the three Open Graph tags, add a CSP because it is one line, shorten the title because it is one edit, and ignore the other two.&lt;/p&gt;

&lt;p&gt;Which is exactly five findings reduced to one opinion. The tool's job was to produce the list. Producing the opinion is still mine.&lt;/p&gt;

</description>
      <category>python</category>
      <category>seo</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Stripping EXIF at the byte level: no re-encoding, no quality loss</title>
      <dc:creator>Danilo Fortunato</dc:creator>
      <pubDate>Thu, 01 Oct 2026 18:12:13 +0000</pubDate>
      <link>https://dev.to/danyblitz/stripping-exif-at-the-byte-level-no-re-encoding-no-quality-loss-5bpo</link>
      <guid>https://dev.to/danyblitz/stripping-exif-at-the-byte-level-no-re-encoding-no-quality-loss-5bpo</guid>
      <description>&lt;h2&gt;
  
  
  The tool everyone's using is quietly damaging your photos
&lt;/h2&gt;

&lt;p&gt;If you strip metadata from an image, the usual approach is to decode it, delete the fields, and encode it again. That is the natural way to do it, because every image library exposes exactly that: a decode step, a field dictionary, an encode step.&lt;/p&gt;

&lt;p&gt;The problem is that JPEG is a &lt;strong&gt;lossy&lt;/strong&gt; format, and the encoder you use is not the encoder that made the file. So a round trip through &lt;code&gt;Pillow&lt;/code&gt; changes the bytes of the image data itself, not just the metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;PIL&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;

&lt;span class="n"&gt;im&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;photo.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;im&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;im&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="c1"&gt;# a fresh, empty image
&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;putdata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;im&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getdata&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;        &lt;span class="c1"&gt;# copy every pixel
&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clean.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;      &lt;span class="c1"&gt;# re-encode at a quality you invented
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now have a file that is &lt;em&gt;not&lt;/em&gt; your original. It is a lossy re-compression of your original, and the damage is not recoverable. The &lt;code&gt;quality=95&lt;/code&gt; is not a neutral default — it is a lossy re-compression of your original. If your original was &lt;code&gt;quality=88&lt;/code&gt;, you just paid twice.&lt;/p&gt;

&lt;p&gt;Stripping metadata should not change the picture. It is a container edit, not an image edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  JPEG is a sequence of segments, and only some of them are the image
&lt;/h2&gt;

&lt;p&gt;A JPEG file is a stream of markers. Each is &lt;code&gt;FF xx&lt;/code&gt;, followed by a 2-byte big-endian length, followed by payload. The pixel data lives in exactly one of them: &lt;code&gt;SOS&lt;/code&gt; (start of scan). Everything before &lt;code&gt;SOS&lt;/code&gt; is metadata containers, and everything from &lt;code&gt;SOS&lt;/code&gt; to &lt;code&gt;FF D9&lt;/code&gt; (EOI) is the compressed scan data.&lt;/p&gt;

&lt;p&gt;That means the whole operation is: copy the bytes, skip the segments you do not want.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;

&lt;span class="c1"&gt;# APPn = the metadata containers; APP0 is JFIF, APP1 is where Exif/XMP live
&lt;/span&gt;&lt;span class="n"&gt;DROP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mh"&gt;0xE1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0xE2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mh"&gt;0xF0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;strip_jpeg_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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="c1"&gt;# SOI
&lt;/span&gt;    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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="c1"&gt;# we are inside the entropy-coded scan: copy the rest verbatim
&lt;/span&gt;            &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0xDA&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="c1"&gt;# SOS: the image itself starts here
&lt;/span&gt;            &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mh"&gt;0xD9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;             &lt;span class="c1"&gt;# EOI
&lt;/span&gt;            &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="n"&gt;seg_len&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;H&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&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="n"&gt;i&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&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="n"&gt;seg_len&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;DROP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two properties fall out of this for free:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;The scan data is never touched&lt;/strong&gt;, so there is no quality loss. Not "low loss" — none. The compressed bytes are the same bytes.&lt;/li&gt;
&lt;li&gt;It cannot leave a partial segment behind, because lengths are read from the file rather than guessed.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  PNG needs a different, simpler rule
&lt;/h2&gt;

&lt;p&gt;PNG is chunks, not segments: an 8-byte length, a 4-byte type, the payload, a 4-byte CRC. Metadata lives in the ancillary chunks &lt;code&gt;tEXt&lt;/code&gt;, &lt;code&gt;zTXt&lt;/code&gt;, &lt;code&gt;iTXt&lt;/code&gt; and &lt;code&gt;eXIf&lt;/code&gt;. The critical chunks (&lt;code&gt;IHDR&lt;/code&gt;, &lt;code&gt;PLTE&lt;/code&gt;, &lt;code&gt;IDAT&lt;/code&gt;, &lt;code&gt;IEND&lt;/code&gt;) and the ancillary ones that affect rendering (&lt;code&gt;tRNS&lt;/code&gt;, &lt;code&gt;gAMA&lt;/code&gt;, &lt;code&gt;cHRM&lt;/code&gt;) must stay.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;

&lt;span class="n"&gt;PNG_DROP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;tEXt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;zTXt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;iTXt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;eXIf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;iter_png_chunks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;                                   &lt;span class="c1"&gt;# skip the PNG signature
&lt;/span&gt;    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;I&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pos&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="n"&gt;ctype&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pos&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="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;length&lt;/span&gt;                   &lt;span class="c1"&gt;# len + type + data + crc
&lt;/span&gt;        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;ctype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;strip_png_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;bytearray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ctype&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;iter_png_chunks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ctype&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PNG_DROP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because every chunk carries its own CRC, dropping whole chunks keeps the file valid — there is no checksum to recompute.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that matters: assert it, do not hope
&lt;/h2&gt;

&lt;p&gt;A metadata stripper that silently fails is worse than none, because you believe you are covered. The cheapest possible insurance is a self-check that builds a file &lt;em&gt;known&lt;/em&gt; to contain the data, strips it, and asserts nothing survives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;PIL&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TiffImagePlugin&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;make_jpeg_with_exif&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# a real 64x64 JPEG carrying a genuine GPS block, written by PIL so the
&lt;/span&gt;    &lt;span class="c1"&gt;# fixture is a file any image library can actually open
&lt;/span&gt;    &lt;span class="n"&gt;im&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RGB&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;160&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;exif&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;im&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getexif&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;gps&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;exif&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_ifd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x8825&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;gps&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;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;N&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;gps&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TiffImagePlugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IFDRational&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;48&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;span class="n"&gt;TiffImagePlugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IFDRational&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;51&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;span class="n"&gt;TiffImagePlugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IFDRational&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2952&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;gps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;E&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;gps&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;4&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="n"&gt;TiffImagePlugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IFDRational&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;TiffImagePlugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IFDRational&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;17&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;span class="n"&gt;TiffImagePlugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;IFDRational&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;buf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;im&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;JPEG&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;quality&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exif&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;exif&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getvalue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;jpeg_scan_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;The compressed image itself: from the start-of-scan marker to the end.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\xff\xda&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;no SOS marker, the fixture is not a JPEG we understand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;make_jpeg_with_exif&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# the fixture must really contain the data, or the test proves nothing
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;getexif&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get_ifd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x8825&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fixture is wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;strip_jpeg_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# and the stripper must really remove it
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;BytesIO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;getexif&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;get_ifd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mh"&gt;0x8825&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GPS survived&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# the image data must be byte-identical, not merely the same size
&lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;jpeg_scan_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;jpeg_scan_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pixels were re-encoded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;self-check OK: EXIF/GPS stripped, pixels preserved&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Both halves have to be asserted. "The metadata is gone" is the easy claim; "the image data is byte-identical" is the claim that actually matters, and it is the one that is normally never checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this lives
&lt;/h2&gt;

&lt;p&gt;I packaged this as &lt;strong&gt;Photo MetaClean&lt;/strong&gt; — standard library only, no dependencies, offline by design, MIT licensed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Windows binary: &lt;a href="https://danyblitz.gumroad.com/l/photo-metaclean" rel="noopener noreferrer"&gt;danyblitz.gumroad.com/l/photo-metaclean&lt;/a&gt; (8 MB, single file, no installer)&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/danyblitz-bit/photo-metaclean" rel="noopener noreferrer"&gt;github.com/danyblitz-bit/photo-metaclean&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The same byte-level treatment for documents is in &lt;a href="https://github.com/danyblitz-bit/pdf-metaclean" rel="noopener noreferrer"&gt;PDF MetaClean&lt;/a&gt;, and the landing page is &lt;a href="https://danyblitz-bit.github.io/photo-metaclean/" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to check on your own photos
&lt;/h2&gt;

&lt;p&gt;Run any of these on a file before you upload it anywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;exiftool photo.jpg
strings photo.jpg | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-40&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you see a GPS block, a camera serial number, your name, or an original filename, it is in the file. Social platforms strip some of it; not all of it, not consistently, and not for every upload path. The part you control is the file you hand over.&lt;/p&gt;

&lt;h2&gt;
  
  
  One caveat worth being honest about
&lt;/h2&gt;

&lt;p&gt;Byte-level copying means the tool does &lt;strong&gt;not&lt;/strong&gt; rewrite anything it does not understand. That is a feature for your image data and a limitation for thumbnails, colour profiles or ICC data you may want to keep. Decide per file: a public upload and a print-ready master have different requirements, and the safe default is to strip for the upload and keep the master untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Photo MetaClean launched on Product Hunt on 2 October 2026. The single-file&lt;br&gt;
Windows binary is on GitHub Releases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/danyblitz-bit/photo-metaclean/releases/tag/v1.1.0" rel="noopener noreferrer"&gt;Release v1.1.0&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.producthunt.com/posts/photo-metaclean" rel="noopener noreferrer"&gt;Product Hunt thread&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;v1.1.0 keeps EXIF Orientation instead of dropping it along with the rest of&lt;br&gt;
APP1, which is what you would have hit on any phone photo.&lt;/p&gt;

&lt;p&gt;If you downloaded v1.0.0, that build drops Orientation as well, so a phone&lt;br&gt;
photo comes out sideways. There is no auto-update: the executable makes no&lt;br&gt;
network connection at all, so nothing will ever tell you a new version exists.&lt;br&gt;
You have to come back for it.&lt;/p&gt;

</description>
      <category>python</category>
      <category>privacy</category>
      <category>security</category>
      <category>imageprocessing</category>
    </item>
  </channel>
</rss>
