<?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: Really Offers</title>
    <description>The latest articles on DEV Community by Really Offers (@reallyoffers).</description>
    <link>https://dev.to/reallyoffers</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%2F4221083%2Fbbaa6666-7af3-416f-9daf-e5b39e0de470.png</url>
      <title>DEV Community: Really Offers</title>
      <link>https://dev.to/reallyoffers</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/reallyoffers"/>
    <language>en</language>
    <item>
      <title>Scoring a "You Won!" Message: The Pattern Checks Behind Our Scam Checkers</title>
      <dc:creator>Really Offers</dc:creator>
      <pubDate>Mon, 02 Nov 2026 08:46:59 +0000</pubDate>
      <link>https://dev.to/reallyoffers/scoring-a-you-won-message-the-pattern-checks-behind-our-scam-checkers-4ce1</link>
      <guid>https://dev.to/reallyoffers/scoring-a-you-won-message-the-pattern-checks-behind-our-scam-checkers-4ce1</guid>
      <description>&lt;p&gt;Prize scams run on scripts. The wording changes, but the skeleton doesn't: you won something, you were chosen, act fast, and — the load-bearing step — pay a small fee or hand over a code to "release" your prize. Regulators like the FTC count the losses from scams that start with a message or a social post in the billions, and most of those losses begin with text a person could have been warned about in the ten seconds before they tapped.&lt;/p&gt;

&lt;p&gt;We built two small checkers around that observation: one scores a pasted message, the other dissects a link. Both are pure client-side pattern checks. This post is about how the scoring works, where it deliberately stops, and what it returns on real inputs — including the ones that make it look bad.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design constraints first
&lt;/h2&gt;

&lt;p&gt;Three decisions shaped everything else:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Pure functions, no DOM, no network.&lt;/strong&gt; &lt;code&gt;smcAnalyze(message, link)&lt;/code&gt; and &lt;code&gt;lrcAnalyze(url)&lt;/code&gt; take strings and return &lt;code&gt;{ score, tells, level, levelLabel }&lt;/code&gt;. Nothing is fetched — critically, the link checker &lt;em&gt;parses&lt;/em&gt; a URL with the &lt;code&gt;URL&lt;/code&gt; constructor but never opens it. A tool for inspecting malicious links that visits malicious links would be a bad tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Every point must be explainable.&lt;/strong&gt; Each hit appends a plain-language "tell" ("It asks you to PAY something…"), and the UI lists them. A risk score the user can't interrogate is just a coloured light.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Three levels, carefully named:&lt;/strong&gt; &lt;code&gt;Looks safer&lt;/code&gt;, &lt;code&gt;Be careful&lt;/code&gt;, &lt;code&gt;High risk — likely a scam pattern&lt;/code&gt;. There is no "Safe" anywhere in the code. That wording choice is a feature, discussed below.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The message model: weighted tells and two decisive flags
&lt;/h2&gt;

&lt;p&gt;The message checker lowercases the message plus an optional link, then runs a table of regex checks through one helper:&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;function&lt;/span&gt; &lt;span class="nf"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isStrong&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;re&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;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;tells&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;label&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;weight&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;isStrong&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;strong&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&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 weights, in order of appearance:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Pattern&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;th&gt;Decisive&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Prize / winner / lottery claim&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"You were (randomly) selected"&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Payment ask: release/processing/delivery fee, gift cards, crypto, wire transfer&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Credential ask: password, OTP, PIN, bank or card details, CVV&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;✔&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shortened link (bit.ly, t.co, rb.gy, …)&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Urgency or threats ("act now", "account will be closed")&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official-sounding impersonation words / "Dear customer"&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pressure to move to WhatsApp/Telegram or reply instantly&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The level logic is one line of consequence:&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;strong&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;level&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="p"&gt;}&lt;/span&gt;      &lt;span class="c1"&gt;// High risk&lt;/span&gt;
&lt;span class="k"&gt;else&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;score&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;level&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="p"&gt;}&lt;/span&gt;      &lt;span class="c1"&gt;// Be careful&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;                     &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;level&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="p"&gt;}&lt;/span&gt;      &lt;span class="c1"&gt;// Looks safer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;strong&lt;/code&gt; flag exists because addition alone gets the ethics wrong. A message whose &lt;em&gt;only&lt;/em&gt; sin is asking for a "release fee" scores just 3 — below the high-risk threshold — yet "pay to receive your prize" is the single most reliable scam signature there is. So payment asks and credential asks are decisive: either one forces High risk on its own, regardless of total.&lt;/p&gt;

&lt;h2&gt;
  
  
  The link model: read the host, not the story
&lt;/h2&gt;

&lt;p&gt;The link checker normalises the input (prepending &lt;code&gt;https://&lt;/code&gt; for parsing if no scheme was pasted), then works almost entirely on the hostname:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;IP-literal host&lt;/strong&gt; or a &lt;code&gt;:&lt;/code&gt; in the host — 3, decisive. Real banks don't send login links as raw numbers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;@&lt;/code&gt; in the authority&lt;/strong&gt; — 3, decisive. Everything before the &lt;code&gt;@&lt;/code&gt; is discarded by the browser; it's a hiding trick.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brand name in the wrong host&lt;/strong&gt; — 3, decisive. A small table maps 15 brands to their real domains (&lt;code&gt;paypal → paypal.com&lt;/code&gt;, &lt;code&gt;amazon → amazon.com&lt;/code&gt;, DHL, Netflix, Binance…). The rule is exact:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;indexOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;brand&lt;/span&gt;&lt;span class="p"&gt;)&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="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;real&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;
    &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&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="nx"&gt;real&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;real&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;hit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;"paypal" appears in the address, but this is NOT the real paypal.com&lt;/span&gt;&lt;span class="dl"&gt;'&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="kc"&gt;true&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;Containing the brand name proves nothing; &lt;em&gt;being&lt;/em&gt; the domain or a subdomain of it is the only pass.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Supporting signals:&lt;/strong&gt; plain &lt;code&gt;http://&lt;/code&gt; (2), a known URL shortener (2), three or more stacked subdomains (2), punycode &lt;code&gt;xn--&lt;/code&gt; (2), the word "bank" in an unlisted domain (2), no visible scheme (1), length over 75 characters (1), three or more hyphens (1), five or more digits (1).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Worked examples — actual outputs
&lt;/h2&gt;

&lt;p&gt;These are the functions' real results (the logic has no DOM dependencies, so it runs unchanged under Node):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Score&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;"Congratulations! You have won a $1000 cash prize. You were randomly selected… pay a small release fee of $25 by gift card within 24 hours. Dear customer, act now — contact us on WhatsApp…"&lt;/td&gt;
&lt;td&gt;10 (6 tells)&lt;/td&gt;
&lt;td&gt;🚩 High risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Hi Sam, your parcel is on its way and should arrive Thursday. You can follow it in the app any time."&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;✅ Looks safer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Your order has shipped. Track your parcel here:" + &lt;code&gt;https://bit.ly/3xYzAbc&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;2 (shortener only)&lt;/td&gt;
&lt;td&gt;⚠️ Be careful&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;http://192.168.1.55/paypal-login&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;6 (http 2 + IP 3 + digit count 1)&lt;/td&gt;
&lt;td&gt;🚩 High risk&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;https://www.amazon.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;✅ Looks safer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;paypal-verify-account.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;4 (no scheme 1 + brand mismatch 3)&lt;/td&gt;
&lt;td&gt;🚩 High risk&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is the design in miniature: the score alone (4) sits in "Be careful" territory, but the decisive brand rule overrides — exactly the case a pure threshold would have under-called.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part we refused to rig
&lt;/h2&gt;

&lt;p&gt;Shortener lists are where a tool like this can quietly cheat, so we didn't: &lt;strong&gt;t.co is on both checkers' shortener lists.&lt;/strong&gt; That's the shortener used by X — and the one used for our own shared links. Paste one of our own shortened links into the Link Checker and it comes back "Be careful: the real destination is hidden until you click." That is, inconveniently, true — for everyone's links, including ours. A warning tool that exempts its author isn't a warning tool.&lt;/p&gt;

&lt;p&gt;The same stance produced the rest of the honesty surface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every result carries the note: &lt;em&gt;"This is a pattern check, not a verdict on this specific sender or website."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;The green state says &lt;strong&gt;"Looks safer"&lt;/strong&gt;, never "Safe" — a formulaic scam written carefully enough can score low, and the UI shouldn't claim otherwise.&lt;/li&gt;
&lt;li&gt;The link checker explicitly defers to Google Safe Browsing and VirusTotal for &lt;em&gt;live&lt;/em&gt; checks, because shape analysis cannot see what a page actually does.&lt;/li&gt;
&lt;li&gt;Empty or unparseable input returns "Be careful" with an explanation — never a pass, never a crash.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What this approach can't do matters as much: regexes are evadable, honest messages trip tells (a genuine delivery text with a shortened tracking link is "Be careful" — by design, because the reader genuinely can't see where it leads), and no pattern score can verify a sender. It's a warning light, not a judge. But warning lights are cheap, instant, and private — and for the ten seconds before someone pays a "release fee," they're often the only check that happens at all.&lt;/p&gt;

&lt;p&gt;Both tools run live as working demos — the &lt;a href="https://guidenestdaily.blogspot.com/p/scam-message-checker-is-this-message.html" rel="noopener noreferrer"&gt;Scam Message Checker&lt;/a&gt; and the companion Link Checker — as pure page snippets: no server, no database, nothing you paste leaves the browser.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>Compressing an Image to an Exact KB Target in the Browser (No Upload, Just Canvas)</title>
      <dc:creator>Really Offers</dc:creator>
      <pubDate>Sun, 01 Nov 2026 18:59:03 +0000</pubDate>
      <link>https://dev.to/reallyoffers/compressing-an-image-to-an-exact-kb-target-in-the-browser-no-upload-just-canvas-4jhb</link>
      <guid>https://dev.to/reallyoffers/compressing-an-image-to-an-exact-kb-target-in-the-browser-no-upload-just-canvas-4jhb</guid>
      <description>&lt;p&gt;Government and exam forms are oddly specific about photos. A typical application portal doesn't say "a small photo, please" — it says the photo must be &lt;strong&gt;under 50KB&lt;/strong&gt;, the signature &lt;strong&gt;under 20KB&lt;/strong&gt;, and the passport photo &lt;strong&gt;exactly 413 × 531 pixels&lt;/strong&gt;. Generic "compress image" tools don't help much here: they hand back whatever size the encoder happens to produce, and if it's 63KB against a 50KB limit, the form rejects it and you start again.&lt;/p&gt;

&lt;p&gt;We needed a tool that treats the KB number as a hard target, not a suggestion — and that never uploads the photo anywhere, because people were feeding it ID-style portraits. The result is a single-page tool built on &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; and one idea: &lt;strong&gt;JPEG file size is monotonic in the quality setting, so you can binary-search it.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Decode and draw
&lt;/h2&gt;

&lt;p&gt;The image is decoded with &lt;code&gt;createImageBitmap()&lt;/code&gt; where available, falling back to an &lt;code&gt;&amp;lt;img&amp;gt;&lt;/code&gt; element with an object URL. It is drawn onto a canvas pre-filled with white:&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="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;#ffffff&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;drawImage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;img&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The white matte matters: the output is always JPEG (the format with a quality dial), so any transparency in a PNG or WebP source has to be flattened onto &lt;em&gt;something&lt;/em&gt; — white is what form photos expect. Everything is re-encoded with &lt;code&gt;canvas.toBlob(cb, "image/jpeg", q)&lt;/code&gt;, where &lt;code&gt;q&lt;/code&gt; is the variable we get to search over.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Probe at minimum quality — then shrink pixels, not hope
&lt;/h2&gt;

&lt;p&gt;The target in bytes is exact: &lt;code&gt;targetBytes = maxKB * 1024&lt;/code&gt;. The first encode at any given size is a probe at the lowest quality, &lt;code&gt;q = 0.05&lt;/code&gt;. If even &lt;em&gt;that&lt;/em&gt; is too big, no quality setting at this resolution will ever fit — the only way down is fewer pixels:&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;var&lt;/span&gt; &lt;span class="nx"&gt;minBlob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;toBlobP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.05&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;minBlob&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;targetBytes&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;t&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;passport&lt;/span&gt;&lt;span class="dl"&gt;"&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="nb"&gt;Math&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="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;best&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;minBlob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="cm"&gt;/* …accept smallest possible… */&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;scale&lt;/span&gt; &lt;span class="o"&gt;*=&lt;/span&gt; &lt;span class="mf"&gt;0.8&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="c1"&gt;// redraw smaller and probe again&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each retry multiplies the scale by 0.8 and redraws. Shrinking dimensions &lt;em&gt;before&lt;/em&gt; touching quality preserves detail where it counts: a 4000 × 3000 phone photo stepped down a few times usually fits a 50KB budget at a respectable quality, while the same photo at full size would need a mushy &lt;code&gt;q&lt;/code&gt; near the floor. The loop stops shrinking when the smallest side reaches 80 px; below that, a form photo isn't recognisable anyway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Binary-search the quality
&lt;/h2&gt;

&lt;p&gt;Once the minimum-quality encode fits, the search walks quality &lt;em&gt;up&lt;/em&gt; to land as close under the cap as possible:&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;var&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.92&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;bestBlob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;minBlob&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;var&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="mi"&gt;7&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;var&lt;/span&gt; &lt;span class="nx"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;)&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="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;toBlobP&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;canvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mid&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;targetBytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;bestBlob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;;&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;Every candidate that fits becomes the new best and moves the lower bound up; every overshoot moves the ceiling down. Seven iterations halve the 0.05–0.92 interval to under 0.01 of quality resolution — more iterations buy almost nothing and each one costs a full encode. The cap of 0.92 is deliberate: past that, JPEG sizes climb steeply for gains nobody can see on a form photo. Because every comparison is against the real &lt;code&gt;blob.size&lt;/code&gt;, the result isn't an estimate — it's a measured file that is actually under the limit.&lt;/p&gt;

&lt;p&gt;The presets are just targets fed into the same engine: max 50KB, 20KB, 100KB, or 200KB, a 20KB ceiling for signatures, and a custom value clamped between 5KB and 5000KB.&lt;/p&gt;

&lt;h2&gt;
  
  
  The passport exception: pixels are the requirement
&lt;/h2&gt;

&lt;p&gt;Passport mode inverts the priorities. The requirement is exact dimensions — 413 × 531 px (35 × 45 mm at 300 DPI) — so the canvas is fixed at that size and the source is cover-cropped into it: scaled by &lt;code&gt;Math.max(t.w / sw, t.h / sh)&lt;/code&gt; and centred, cropping the overflow. The dimension step-down is skipped entirely; only the quality search runs, against a 200KB ceiling that a 413 × 531 JPEG effectively always meets. Getting the crop or the aspect wrong fails the application, so this mode refuses to be clever about size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Edge cases, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Already under the limit?&lt;/strong&gt; The tool still re-encodes: the search walks quality up toward 0.92 and returns the best version that fits. The output is always a fresh JPEG, never the untouched original.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An impossible target.&lt;/strong&gt; If a huge, noisy photo still exceeds the cap at minimum quality and minimum size, the tool returns the smallest file it could make and &lt;em&gt;says so&lt;/em&gt; — "Closest possible is X" — instead of pretending the target was met.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;EXIF orientation is not handled explicitly.&lt;/strong&gt; Decoding is delegated to the browser, and the re-encode drops the original metadata. That's a real limitation to test against your target browsers, not a detail to hand-wave.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Encoding can fail.&lt;/strong&gt; A &lt;code&gt;null&lt;/code&gt; blob from &lt;code&gt;toBlob&lt;/code&gt; surfaces as a plain "please try another photo" message rather than a silent dead button.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Privacy is a side effect of the architecture
&lt;/h2&gt;

&lt;p&gt;There is no upload endpoint to trust or distrust: the code contains no network calls at all. The file is decoded locally, the preview and the download link are both blob object URLs, and the previous result's URL is revoked when a new one is generated. "Your photo never leaves your device" is printed on the page — and for once it's not a policy statement, it's a description of the code path.&lt;/p&gt;

&lt;p&gt;If you want to see the search run against a real photo, the live tool is here: &lt;a href="https://guidenestdaily.blogspot.com/p/compress-image-to-50kb-online-free.html" rel="noopener noreferrer"&gt;Compress Image to 50KB Online Free&lt;/a&gt;. The whole engine is the snippet above plus drawing code — small enough that the interesting part isn't the size of the code, it's treating a bureaucratic KB limit as a search problem instead of a slider.&lt;/p&gt;

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