<?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: shweta dinkar</title>
    <description>The latest articles on DEV Community by shweta dinkar (@shwetadinkar).</description>
    <link>https://dev.to/shwetadinkar</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%2F4122175%2F4342c5f7-70e9-4706-b712-6f5e46e9b457.png</url>
      <title>DEV Community: shweta dinkar</title>
      <link>https://dev.to/shwetadinkar</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/shwetadinkar"/>
    <language>en</language>
    <item>
      <title>What WhatsApp, Instagram and Telegram actually do to your photos</title>
      <dc:creator>shweta dinkar</dc:creator>
      <pubDate>Sat, 12 Sep 2026 13:12:44 +0000</pubDate>
      <link>https://dev.to/shwetadinkar/what-whatsapp-instagram-and-telegram-actually-do-to-your-photos-2gjd</link>
      <guid>https://dev.to/shwetadinkar/what-whatsapp-instagram-and-telegram-actually-do-to-your-photos-2gjd</guid>
      <description>&lt;p&gt;I spent a month measuring it, one round trip at a time, on a real phone. Here is&lt;br&gt;
what came back.&lt;/p&gt;

&lt;p&gt;Everyone knows chat apps "compress" photos. Almost nobody knows &lt;em&gt;how&lt;/em&gt;, because&lt;br&gt;
the answer is not in any documentation — you have to send an image, download&lt;br&gt;
what returns, and read the JPEG header. So that is what I did, several hundred&lt;br&gt;
times, and the results were more specific and more useful than I expected.&lt;/p&gt;


&lt;h2&gt;
  
  
  Every platform has a fingerprint
&lt;/h2&gt;

&lt;p&gt;A JPEG carries the quantization table it was encoded with. Most software uses a&lt;br&gt;
scaled version of the reference table in the JPEG standard's Annex K — pick a&lt;br&gt;
quality number, the table scales, done. So if you know what Annex K looks like,&lt;br&gt;
you can usually tell what encoded a file.&lt;/p&gt;

&lt;p&gt;Two of these platforms do not use Annex K at all.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WhatsApp, standard send.&lt;/strong&gt; Luma table, min 6, max 167, mean 35.6:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   6    6    6    7   10   15   22   34
   6    7    8   11   14   16   21   30
   6    8   10   12   17   25   36   54
   7   11   12   16   21   30   42   62
  10   14   17   21   28   38   52   76
  15   16   25   30   38   50   68   95
  22   21   36   42   52   68   90  124
  34   30   54   62   76   95  124  167
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not Annex K scaled. The best-fit scale is 0.650 — nominally around&lt;br&gt;
quality 68 — but the per-coefficient ratio ranges from 0.28 to 1.69, with a 37%&lt;br&gt;
mean residual. The &lt;em&gt;average&lt;/em&gt; step is close to quality 65; the &lt;em&gt;shape&lt;/em&gt; is&lt;br&gt;
completely different, with a much steeper roll-off. Low frequencies are barely&lt;br&gt;
touched at 6 to 12, and the far corner is crushed at 167.&lt;/p&gt;

&lt;p&gt;It is also &lt;strong&gt;content-independent&lt;/strong&gt;. I saw the identical table across 82 returned&lt;br&gt;
files spanning five different output geometries. Not similar — byte-identical.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instagram.&lt;/strong&gt; Min 5, max 25, mean 15.8, and progressive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   5    6   11   11   13   14   15   16
   6    8   11   11   14   13   16   16
  11   11   11   13   14   16   15   17
  11   11   13   14   17   19   19   19
  13   14   14   17   18   20   22   22
  14   13   16   19   20   22   21   25
  15   16   15   19   22   21   22   22
  16   16   17   19   22   25   22   18
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Very flat, very gentle. And &lt;strong&gt;Facebook returns a byte-identical table&lt;/strong&gt; — same&lt;br&gt;
Meta pipeline, so you cannot tell an Instagram file from a Facebook one by its&lt;br&gt;
quantization table alone. Only the geometry separates them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Telegram&lt;/strong&gt; is the ordinary case: a scaled Annex K near quality 87. Nothing&lt;br&gt;
custom.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WhatsApp HD&lt;/strong&gt; is a different table again, min 4, max 19, mean 12.2 — much&lt;br&gt;
finer than the standard send, which is what you would hope from a mode called&lt;br&gt;
HD.&lt;/p&gt;


&lt;h2&gt;
  
  
  Geometry is the part that actually matters
&lt;/h2&gt;

&lt;p&gt;The tables are interesting. The geometry decides whether anything works.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;What it does to the frame&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp, standard&lt;/td&gt;
&lt;td&gt;Caps the &lt;strong&gt;long edge&lt;/strong&gt; at 1600. At or below, dimensions pass through untouched&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WhatsApp, HD on&lt;/td&gt;
&lt;td&gt;Keeps 4096 — a 4096×3072 photo returns 4096×3072&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telegram, as photo&lt;/td&gt;
&lt;td&gt;Re-encodes &lt;strong&gt;everything&lt;/strong&gt; to 1280×960&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Telegram, as file&lt;/td&gt;
&lt;td&gt;Untouched. Byte-identical, in my tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Instagram&lt;/td&gt;
&lt;td&gt;Does not cap — &lt;strong&gt;normalises onto a 1440×1440 square canvas&lt;/strong&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Facebook&lt;/td&gt;
&lt;td&gt;2048 long edge&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X/Twitter&lt;/td&gt;
&lt;td&gt;4096 long edge&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of these surprised me.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Instagram does not have a maximum size, it has a shape.&lt;/strong&gt; Upload something&lt;br&gt;
1080 wide and it comes back &lt;em&gt;upscaled&lt;/em&gt; to 1440. Upload 4:3 and it comes back&lt;br&gt;
padded to square. There is no size small enough to be safe, because the&lt;br&gt;
normalisation is not a cap.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WhatsApp caps the long edge, not the width.&lt;/strong&gt; A portrait photo comes back&lt;br&gt;
1200×1600, not 1600×1200. Obvious in hindsight; not obvious when your code says&lt;br&gt;
&lt;code&gt;width&lt;/code&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  The bit that bites: EXIF orientation
&lt;/h2&gt;

&lt;p&gt;Send a photo your phone took in portrait, and there is a good chance it is&lt;br&gt;
stored as landscape pixels with an EXIF flag saying "rotate this 90°".&lt;/p&gt;

&lt;p&gt;WhatsApp applies that flag &lt;em&gt;physically&lt;/em&gt; and strips it. I sent a 3840×2160 file&lt;br&gt;
with &lt;code&gt;orientation=6&lt;/code&gt; and got back 2160×3840 with no orientation tag. The pixels&lt;br&gt;
were rotated, not relabelled.&lt;/p&gt;

&lt;p&gt;For ordinary photos this is invisible and correct. For anything that depends on&lt;br&gt;
where pixels sit — watermarking, hidden data, forensic comparison — a 90°&lt;br&gt;
rotation is total loss, and it happens silently to a photo the user shot&lt;br&gt;
normally.&lt;/p&gt;


&lt;h2&gt;
  
  
  Why any of this matters
&lt;/h2&gt;

&lt;p&gt;I was measuring all this because I was building steganography: hiding data&lt;br&gt;
inside a photo so it survives being sent through a chat app. That turns out to&lt;br&gt;
be almost entirely a geometry problem.&lt;/p&gt;

&lt;p&gt;Hidden data in JPEG lives in the 8×8 DCT blocks the format is built on. If a&lt;br&gt;
platform &lt;strong&gt;resamples&lt;/strong&gt; the image, the spacing of that grid changes, the decoder&lt;br&gt;
starts reading across block boundaries, and the payload is not degraded — it is&lt;br&gt;
&lt;em&gt;destroyed&lt;/em&gt;. Every catastrophic failure I measured came out at roughly 50% bit&lt;br&gt;
error rate, which is the same as guessing.&lt;/p&gt;

&lt;p&gt;So the whole game is not stronger error correction. It is handing the platform&lt;br&gt;
an image it has no reason to touch. Match its output geometry and the grid&lt;br&gt;
survives intact.&lt;/p&gt;

&lt;p&gt;Then there is a second, subtler win. If you encode your image using &lt;strong&gt;the&lt;br&gt;
platform's own quantization table&lt;/strong&gt;, its re-encode has nothing left to change.&lt;br&gt;
I extracted Meta's table from images Instagram returned and encoded on it&lt;br&gt;
directly. The share of the embedding band that survived went from 85.9% to&lt;br&gt;
100%, and — the part I did not expect — the required embedding strength halved,&lt;br&gt;
so the image came out &lt;em&gt;visibly cleaner&lt;/em&gt; as well as more robust.&lt;/p&gt;


&lt;h2&gt;
  
  
  Things I got wrong
&lt;/h2&gt;

&lt;p&gt;Worth recording, since the wrong turns took longer than the right ones.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PSNR is misleading for this.&lt;/strong&gt; It ranked a visibly dotted image &lt;em&gt;above&lt;/em&gt; a&lt;br&gt;
clean one, because it averages squared error across the whole frame and cannot&lt;br&gt;
see that the error is concentrated in the flat areas where the eye looks. If you&lt;br&gt;
are comparing image quality on PSNR alone, be suspicious — including of results&lt;br&gt;
that flatter you.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A texture-adaptive step size made things worse, not better.&lt;/strong&gt; The idea was&lt;br&gt;
sound: perturb more where texture hides it, less on flat walls and sky. What&lt;br&gt;
actually happened is that after hard recompression, blocks shift between&lt;br&gt;
texture bands, so the decoder reads whole blocks at the wrong strength. That is&lt;br&gt;
&lt;em&gt;whole-block&lt;/em&gt; error, and both Reed–Solomon and bit repetition assume errors are&lt;br&gt;
sparse. Three separate parameters all plateaued at exactly the same point, which&lt;br&gt;
is what finally gave it away. Turning the feature off fixed it — at a &lt;em&gt;smaller&lt;/em&gt;&lt;br&gt;
step, so the images got quieter too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simulated channels flatter you.&lt;/strong&gt; A resize-plus-re-encode simulator made&lt;br&gt;
Instagram look like the easy case. On a real account it was the hardest, because&lt;br&gt;
its damage is sharpening, not compression, and no quality setting reproduces&lt;br&gt;
that.&lt;/p&gt;


&lt;h2&gt;
  
  
  Reproduce it yourself
&lt;/h2&gt;

&lt;p&gt;None of this needs special equipment: a phone, a few photos, and Pillow.&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;numpy&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;np&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;returned_from_whatsapp.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;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;array&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;quantization&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="nf"&gt;reshape&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="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&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="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Send an image, download what comes back — the actual file, never a screenshot —&lt;br&gt;
and read the table. If it matches one of the fingerprints above, you have an&lt;br&gt;
authentic platform return. If it fits a scaled Annex K almost perfectly,&lt;br&gt;
something re-encoded it on the way and whatever you measure from it is not the&lt;br&gt;
platform's behaviour.&lt;/p&gt;

&lt;p&gt;That last point cost other people real results in a contest I was part of:&lt;br&gt;
calibration samples were shared through a channel that quietly downscaled them&lt;br&gt;
to 1024px, and the measurements had to be withdrawn. The irony was not lost on&lt;br&gt;
anyone.&lt;/p&gt;


&lt;h2&gt;
  
  
  Try the round trip, not the demo
&lt;/h2&gt;

&lt;p&gt;All of this went into &lt;strong&gt;Stegstr&lt;/strong&gt;, an open-source app that hides a message — or&lt;br&gt;
a whole feed — inside a photo, in a way that survives WhatsApp, Telegram,&lt;br&gt;
Instagram, Facebook and X. It is local-first: embedding and detecting happen on&lt;br&gt;
your machine, and the platform carrying the image never knows it carried&lt;br&gt;
anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://stegstr.com/r/UE8635" rel="noopener noreferrer"&gt;https://stegstr.com/r/UE8635&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One thing worth knowing before you start: &lt;strong&gt;hiding a message in a photo is the&lt;br&gt;
boring half.&lt;/strong&gt; Anyone can do that. The interesting half is whether it comes back&lt;br&gt;
out after a platform has had it — and you cannot see that by embedding an image&lt;br&gt;
and looking at it.&lt;/p&gt;

&lt;p&gt;There is a browser version that embeds and detects with nothing leaving the&lt;br&gt;
page, which is the fastest way to see the mechanism. But it does not do the&lt;br&gt;
part worth seeing. For the full loop you want the desktop app:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Install it&lt;/strong&gt; from the link above — Mac, Windows, Linux.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn Network on.&lt;/strong&gt; It ships off, and in local mode nothing is sent
anywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post a note.&lt;/strong&gt; Anything. This is what ends up inside the photo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Embed your feed into a photo&lt;/strong&gt;, choosing the platform you are about to send
it through. That choice sets the geometry, and it is the thing this whole
article is about.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Send the image to someone&lt;/strong&gt; through that platform — as a photo, not as a
file, or you have skipped the compression entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Have them open it&lt;/strong&gt; at the other end.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If it works, they see your notes, recovered from a photo that went through&lt;br&gt;
WhatsApp's re-encoder. If it fails, you have learned something specific about a&lt;br&gt;
platform, and I would like to hear about it — the measurements above are one&lt;br&gt;
device and one session each, and the honest limit of this work is that nobody&lt;br&gt;
has repeated it.&lt;/p&gt;

&lt;p&gt;Two people sending images back and forth is the actual use case, and also the&lt;br&gt;
only way to find out whether any of this generalises beyond my phone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you would rather not install a GUI&lt;/strong&gt;, there is a CLI and an MCP server&lt;br&gt;
exposing the same encoder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/brunkstr/Stegstr.git &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;Stegstr/src-tauri
cargo build &lt;span class="nt"&gt;--release&lt;/span&gt; &lt;span class="nt"&gt;--bin&lt;/span&gt; stegstr-cli
./target/release/stegstr-cli post &lt;span class="s2"&gt;"hello from a script"&lt;/span&gt; &lt;span class="nt"&gt;--ref&lt;/span&gt; UE8635 &lt;span class="nt"&gt;--json&lt;/span&gt;
./target/release/stegstr-cli embed cover.jpg &lt;span class="nt"&gt;-o&lt;/span&gt; out.jpg &lt;span class="nt"&gt;--payload&lt;/span&gt; &lt;span class="s2"&gt;"hidden"&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is also a dependency-free Node CLI (&lt;code&gt;node dist-cli/stegstr.mjs&lt;/code&gt;) that&lt;br&gt;
needs no compiler, and an MCP server (&lt;code&gt;stegstr-cli mcp&lt;/code&gt;) if you want an agent&lt;br&gt;
to do the embedding.&lt;/p&gt;

&lt;p&gt;The measurements above, and the returned files behind them, are committed in the&lt;br&gt;
repository. You do not have to take my word for any of it.&lt;/p&gt;

</description>
      <category>security</category>
      <category>opensource</category>
      <category>python</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
