<?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: Food Feed Studio</title>
    <description>The latest articles on DEV Community by Food Feed Studio (@food_feedstudio).</description>
    <link>https://dev.to/food_feedstudio</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%2F4026840%2Fb1381de4-a718-484c-89d8-57810f9888b4.png</url>
      <title>DEV Community: Food Feed Studio</title>
      <link>https://dev.to/food_feedstudio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/food_feedstudio"/>
    <language>en</language>
    <item>
      <title>Every image compressor only goes one way. That's why your form upload keeps failing.</title>
      <dc:creator>Food Feed Studio</dc:creator>
      <pubDate>Mon, 13 Jul 2026 07:34:13 +0000</pubDate>
      <link>https://dev.to/food_feedstudio/every-image-compressor-only-goes-one-way-thats-why-your-form-upload-keeps-failing-lba</link>
      <guid>https://dev.to/food_feedstudio/every-image-compressor-only-goes-one-way-thats-why-your-form-upload-keeps-failing-lba</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fui2mn2bmogev6imv3agq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fui2mn2bmogev6imv3agq.png" alt=" " width="800" height="479"&gt;&lt;/a&gt;There's a bug in an entire category of software, and once you see it you can't unsee it.&lt;/p&gt;

&lt;p&gt;Government and university application portals ask for a photo like this:&lt;br&gt;
*&lt;em&gt;Photograph: 200×230 pixels, between 20 KB and 50 KB, JPEG only.&lt;br&gt;
*&lt;/em&gt;&lt;br&gt;
Read that carefully. It's not a maximum. It's a range.&lt;/p&gt;

&lt;p&gt;Now go and look at what every image compressor on the internet actually does. imagecompressor, easyphoto, shrink.media, instasize, picssizer, fixmykb — they all give you one box: "target size", or "max KB". Some of the better ones (IMGonline, Img2Go, the desktop tool RIOT) will let you aim at an exact size, which is genuinely better.&lt;/p&gt;

&lt;p&gt;But not one of them takes a range.&lt;/p&gt;

&lt;p&gt;So you compress your photo, it comes out at 8 KB, and the portal rejects it — for being too small. And the error message says something like Invalid photograph, which tells you precisely nothing.&lt;/p&gt;

&lt;p&gt;It gets worse when you follow the instructions&lt;/p&gt;

&lt;p&gt;Here's the part that turns an annoyance into a trap.&lt;/p&gt;

&lt;p&gt;The form also told you it wants 200×230 pixels. So you resize to exactly that. And at 200×230, a clean JPEG of a face is naturally about 10–15 KB. It falls under the 20 KB floor on its own, before you've compressed anything at all.&lt;/p&gt;

&lt;p&gt;Now you're stuck in a genuinely impossible loop, because compressing is the wrong direction. You don't need the file smaller. You need it bigger. And there is no image tool anywhere that makes a file bigger, because "make this file bigger" has never been a thing anyone wanted — until a bureaucrat wrote a spec with a floor in it.&lt;/p&gt;

&lt;p&gt;This, I'm now fairly sure, is the real reason behind a category of forum post I'd seen for years and never understood:&lt;/p&gt;

&lt;p&gt;"I am facing problem in uploading my photo. The size, background, type — everything is perfect but as soon as I upload it, no notification pops and just everything clears up." — someone on Quora, about JEE Mains&lt;/p&gt;

&lt;p&gt;Everything is perfect. That's the problem. They checked every rule they could see, and the one that's failing is the one nobody mentions.&lt;/p&gt;

&lt;p&gt;So how do you make a JPEG bigger?&lt;/p&gt;

&lt;p&gt;Two ways, and the order matters.&lt;/p&gt;

&lt;p&gt;First, honestly: turn the quality up.&lt;/p&gt;

&lt;p&gt;The usual approach to hitting a size target is to binary-search JPEG quality downward until you're under the ceiling. But if you search for the largest file that still fits under the max — rather than the smallest — you often clear the minimum for free, with better image quality as a bonus:&lt;/p&gt;

&lt;p&gt;let lo = 0.05, hi = 1.0, best = null;&lt;br&gt;
for (let i = 0; i &amp;lt; 10; i++) {&lt;br&gt;
  const mid = (lo + hi) / 2;&lt;br&gt;
  const blob = await encodeJpeg(canvas, mid);&lt;br&gt;
  if (blob.size &amp;lt;= maxBytes) {&lt;br&gt;
    best = blob;   // keep it, and try to go BIGGER&lt;br&gt;
    lo = mid;&lt;br&gt;
  } else {&lt;br&gt;
    hi = mid;&lt;br&gt;
  }&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;That's it. Same binary search everyone writes, just optimising for the opposite end. It costs nothing and it fixes most cases, because the extra bytes are real image data — you're handing the portal a better photo, not a padded one.&lt;/p&gt;

&lt;p&gt;Second, when the form's own rules contradict each other.&lt;/p&gt;

&lt;p&gt;Sometimes even quality 1.0 at 200×230 can't reach 20 KB. The form is demanding small dimensions and a large minimum file size, and for this particular photo those two rules are simply incompatible. There is no honest image you can produce that satisfies both.&lt;/p&gt;

&lt;p&gt;At that point you have to add bytes that aren't pixels. And JPEG has a place to put them: the COM (comment) segment, marker 0xFFFE. Every decoder on earth skips it.&lt;/p&gt;

&lt;p&gt;function padJpegToMinimum(bytes, minBytes) {&lt;br&gt;
  const shortfall = minBytes - bytes.length;&lt;br&gt;
  if (shortfall &amp;lt;= 0) return bytes;&lt;/p&gt;

&lt;p&gt;// COM segment: FF FE &amp;lt;2-byte big-endian length&amp;gt; &lt;br&gt;
  // The length counts itself, so payload = length - 2, capped at 0xFFFF.&lt;br&gt;
  const MAX_PAYLOAD = 0xffff - 2;&lt;br&gt;
  const segments = [];&lt;br&gt;
  let remaining = shortfall;&lt;/p&gt;

&lt;p&gt;while (remaining &amp;gt; 0) {&lt;br&gt;
    const payload = Math.min(remaining &amp;gt; 4 ? remaining - 4 : 1, MAX_PAYLOAD);&lt;br&gt;
    const length = payload + 2;&lt;br&gt;
    segments.push(0xff, 0xfe, (length &amp;gt;&amp;gt; 8) &amp;amp; 0xff, length &amp;amp; 0xff);&lt;br&gt;
    for (let i = 0; i &amp;lt; payload; i++) segments.push(0x20); // spaces&lt;br&gt;
    remaining -= payload + 4;&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;// Insert straight after SOI (FF D8).&lt;br&gt;
  const out = new Uint8Array(bytes.length + segments.length);&lt;br&gt;
  out.set(bytes.subarray(0, 2), 0);&lt;br&gt;
  out.set(segments, 2);&lt;br&gt;
  out.set(bytes.subarray(2), 2 + segments.length);&lt;br&gt;
  return out;&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;Not one pixel changes. The image a human sees is bit-for-bit identical to the one at quality 1.0. You're satisfying a byte-count check, and nothing else.&lt;/p&gt;

&lt;p&gt;Is that cheating? I'd argue no — but I also think you have to say you're doing it, so the tool prints a note on screen whenever it pads: "At 200×230 this photo is naturally smaller than the 20 KB minimum even at full quality, so we padded the file's metadata to clear it. Not one pixel was changed." If a tool quietly manipulates a file to pass a validation check and doesn't tell you, that's the bit I'd have a problem with.&lt;/p&gt;

&lt;p&gt;How to be sure you didn't corrupt it&lt;/p&gt;

&lt;p&gt;The test that gave me confidence walks the output's own segment markers to find the SOF (start of frame) header and reads the real dimensions back out:&lt;/p&gt;

&lt;p&gt;function readJpegSize(buf) {&lt;br&gt;
  if (buf[0] !== 0xff || buf[1] !== 0xd8) throw new Error("not a JPEG");&lt;br&gt;
  let i = 2;&lt;br&gt;
  while (i &amp;lt; buf.length) {&lt;br&gt;
    if (buf[i] !== 0xff) throw new Error("desynced — segment lengths are wrong");&lt;br&gt;
    const marker = buf[i + 1];&lt;br&gt;
    if (marker &amp;gt;= 0xc0 &amp;amp;&amp;amp; marker &amp;lt;= 0xcf &amp;amp;&amp;amp; marker !== 0xc4 &amp;amp;&amp;amp; marker !== 0xc8) {&lt;br&gt;
      return { height: buf.readUInt16BE(i + 5), width: buf.readUInt16BE(i + 7) };&lt;br&gt;
    }&lt;br&gt;
    i += 2 + buf.readUInt16BE(i + 2);&lt;br&gt;
  }&lt;br&gt;
  throw new Error("no SOF marker");&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;To reach the SOF it has to correctly skip every segment you inserted. Get a single length byte wrong and the walk desynchronises and throws. A corrupt photo that passed a size check would be far worse than shipping nothing.&lt;/p&gt;

&lt;p&gt;The other one: your file isn't the format you think it is&lt;/p&gt;

&lt;p&gt;While I was in there, a second cause of the same silent failure turned up.&lt;/p&gt;

&lt;p&gt;iPhones shoot HEIC by default. Most portals only accept JPEG. Fine — except that AirDrop, WhatsApp and Windows Photos will all cheerfully hand you a file called photo.jpg that is still HEIC inside.&lt;/p&gt;

&lt;p&gt;So people are entirely certain their file is a JPEG. It says .jpg. And the upload fails silently.&lt;/p&gt;

&lt;p&gt;Check the bytes, not the name. HEIC is ISO-BMFF: bytes 4–8 are the literal string ftyp, and the brand right after tells you the flavour:&lt;/p&gt;

&lt;p&gt;async function isHeic(file) {&lt;br&gt;
  const head = new Uint8Array(await file.slice(0, 12).arrayBuffer());&lt;br&gt;
  if (String.fromCharCode(...head.subarray(4, 8)) !== "ftyp") return false;&lt;br&gt;
  const brand = String.fromCharCode(...head.subarray(8, 12));&lt;br&gt;
  return ["heic","heix","hevc","hevx","mif1","msf1"].includes(brand);&lt;br&gt;
}&lt;/p&gt;

&lt;p&gt;One more trap here, and I fell straight into it: createImageBitmap() decodes HEIC on Safari and iOS, and not on Chrome or Firefox. So my HEIC support worked perfectly when I tested it on an iPhone, and was completely broken for anyone who'd AirDropped the photo to a laptop first — which is exactly what people do when they're filling in a form. It shipped that way, advertised on the tin, for longer than I'd like.&lt;/p&gt;

&lt;p&gt;The thing I decided not to build&lt;/p&gt;

&lt;p&gt;Every site in this niche ships a table of per-exam presets: "SSC: 200×230px, 20–50 KB". It's the obvious feature. It's free SEO — one landing page per exam, dozens of pages.&lt;/p&gt;

&lt;p&gt;I went to build it, and found that the presets contradict each other. For one railway exam, one site published 200×230px / 20–50 KB and another published 320×240px / 40–100 KB. They can't both be right. The official notifications frequently don't state fixed numbers at all — they tell candidates to follow the live upload form. And the specs change between recruitment cycles anyway.&lt;/p&gt;

&lt;p&gt;A wrong number there doesn't give someone a worse photo. It gets their application rejected.&lt;/p&gt;

&lt;p&gt;So I shipped no preset table, and the tool asks you to read the two numbers off your own form instead. It was the easiest forty pages I could have added to the site and I didn't take them, which I mention only because I think it's the interesting decision, not the noble one — if your data source is provably self-contradictory, "ship it anyway with a confident UI" is just lying with extra steps.&lt;/p&gt;

&lt;p&gt;It runs in your browser, and you can check&lt;/p&gt;

&lt;p&gt;All of the above is client-side. Not as a feature, but because an ID photo is exactly the file you shouldn't have to upload to a stranger's server in order to resize.&lt;/p&gt;

&lt;p&gt;The trouble is that "we don't upload your files" is what every file-tool site says, including the ones that upload your files. It's unfalsifiable, so it convinces nobody who's actually worried.&lt;/p&gt;

&lt;p&gt;So: turn off your Wi-Fi. It still works. A site that runs with the network off can't be secretly sending your documents anywhere, and that takes five seconds to check without trusting a word I've said. It's a service worker and about 230 KB of precached shell — the cheapest trust signal I've ever shipped.&lt;/p&gt;

&lt;p&gt;The tool&lt;/p&gt;

&lt;p&gt;It's free, there's no signup, and it does the whole rule rather than half of it: exact pixel dimensions, both ends of the KB range, HEIC in and JPEG out.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://docexp.com/tool/photo-upload-fixer" rel="noopener noreferrer"&gt;https://docexp.com/tool/photo-upload-fixer&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you've ever had an upload rejected by a form that wouldn't tell you why, it was probably the minimum.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Common targets, if you just want the number: &lt;br&gt;
&lt;a href="https://docexp.com/compress-image-to-20kb" rel="noopener noreferrer"&gt;20 KB&lt;/a&gt; · &lt;br&gt;
&lt;a href="https://docexp.com/compress-image-to-50kb" rel="noopener noreferrer"&gt;50 KB&lt;/a&gt; · &lt;br&gt;
&lt;a href="https://docexp.com/compress-image-to-100kb" rel="noopener noreferrer"&gt;100 KB&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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