<?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: byeval</title>
    <description>The latest articles on DEV Community by byeval (@byeval).</description>
    <link>https://dev.to/byeval</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F126888%2F9d67c3f9-8e1b-4109-aeac-a238f1342e7d.png</url>
      <title>DEV Community: byeval</title>
      <link>https://dev.to/byeval</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/byeval"/>
    <language>en</language>
    <item>
      <title>How To Auto-Detect QR Codes, Signatures, and License Plates In The Browser</title>
      <dc:creator>byeval</dc:creator>
      <pubDate>Wed, 22 Apr 2026 13:51:26 +0000</pubDate>
      <link>https://dev.to/byeval/how-to-auto-detect-qr-codes-signatures-and-license-plates-in-the-browser-2e58</link>
      <guid>https://dev.to/byeval/how-to-auto-detect-qr-codes-signatures-and-license-plates-in-the-browser-2e58</guid>
      <description>&lt;p&gt;One of the easiest mistakes in privacy tooling is trying to solve every target type with one detector.&lt;/p&gt;

&lt;p&gt;QR codes, signatures, and license plates all end up as "regions to hide," but technically they are different problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;QR codes are machine-readable symbols&lt;/li&gt;
&lt;li&gt;license plates are structured text with strong visual constraints&lt;/li&gt;
&lt;li&gt;signatures are image shapes more than readable words&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you force all three through generic OCR, the output gets noisy fast.&lt;/p&gt;

&lt;p&gt;The companion guide for this piece is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://happyimg.com/guides/how-to-auto-detect-qr-codes-signatures-and-license-plates-in-the-browser" rel="noopener noreferrer"&gt;https://happyimg.com/guides/how-to-auto-detect-qr-codes-signatures-and-license-plates-in-the-browser&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Mixed detection works better than one universal pipeline
&lt;/h2&gt;

&lt;p&gt;From the product side, these features all look related. The user wants the tool to suggest privacy-sensitive regions automatically.&lt;/p&gt;

&lt;p&gt;From the engineering side, they need different signals.&lt;/p&gt;

&lt;p&gt;The more useful architecture is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple detector functions&lt;/li&gt;
&lt;li&gt;one normalized region format&lt;/li&gt;
&lt;li&gt;one editor surface&lt;/li&gt;
&lt;li&gt;one review step before export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That keeps the interaction model simple without pretending the detection problem is simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  QR codes and barcodes: use the browser when the browser already knows
&lt;/h2&gt;

&lt;p&gt;For QR codes and barcodes, the cleanest path is usually &lt;code&gt;BarcodeDetector&lt;/code&gt; when the browser supports it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;detector&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BarcodeDetector&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;qr_code&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;code_128&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ean_13&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pdf417&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;detector&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;detect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gives you native symbol detection plus bounding boxes you can pad into safer blur or redaction regions.&lt;/p&gt;

&lt;p&gt;The product lesson here is mostly about failure modes. If &lt;code&gt;BarcodeDetector&lt;/code&gt; is unavailable, the UI should say so explicitly. Silent failure is worse than no feature because it makes the user trust an absence of results that may be false.&lt;/p&gt;

&lt;h2&gt;
  
  
  License plates: OCR is useful, but only as a candidate generator
&lt;/h2&gt;

&lt;p&gt;License plates are text, but not ordinary text. A raw OCR pass usually gives too much junk unless you filter aggressively.&lt;/p&gt;

&lt;p&gt;The pattern we used is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;start from OCR blocks and lines&lt;/li&gt;
&lt;li&gt;normalize candidate text to uppercase alphanumeric characters&lt;/li&gt;
&lt;li&gt;require both letters and digits&lt;/li&gt;
&lt;li&gt;filter by plausible string length&lt;/li&gt;
&lt;li&gt;reject impossible aspect ratios&lt;/li&gt;
&lt;li&gt;ignore text in unlikely vertical positions&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That turns OCR into a candidate generator instead of pretending it understands the full context of a vehicle image.&lt;/p&gt;

&lt;p&gt;This is often the right level of ambition for privacy tooling: narrow heuristics on top of a broad detector.&lt;/p&gt;

&lt;h2&gt;
  
  
  Signatures: image analysis beats text recognition
&lt;/h2&gt;

&lt;p&gt;Signatures are the opposite case. OCR often performs poorly because handwriting is inconsistent and the goal is not to read the text anyway. The goal is to find the signed region.&lt;/p&gt;

&lt;p&gt;So the better signal was image analysis on a scaled canvas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;imageData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;context&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getImageData&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;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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;threshold&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;estimateSignatureThreshold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From there, the detector walks connected dark components, measures each region, and filters by heuristics like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;width&lt;/li&gt;
&lt;li&gt;height&lt;/li&gt;
&lt;li&gt;fill ratio&lt;/li&gt;
&lt;li&gt;relative position on the page&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then nearby candidates can be merged into one more useful region.&lt;/p&gt;

&lt;p&gt;This is not a universal signature model, and that is exactly the point. It is a practical heuristic for one narrow job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Detection quality depends on what happens after detection
&lt;/h2&gt;

&lt;p&gt;Even when the detector logic is correct, the raw output is usually not ready for users.&lt;/p&gt;

&lt;p&gt;The post-processing layer matters a lot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;add padding so the target is fully covered&lt;/li&gt;
&lt;li&gt;merge nearby fragments&lt;/li&gt;
&lt;li&gt;deduplicate overlapping results&lt;/li&gt;
&lt;li&gt;normalize everything into the same region shape the editor understands&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you skip those steps, the result is usually a screen full of tiny, fragmented boxes that nobody trusts.&lt;/p&gt;

&lt;h2&gt;
  
  
  One review model for many detectors
&lt;/h2&gt;

&lt;p&gt;The biggest architecture win was not in the detectors themselves. It was in the shared output model.&lt;/p&gt;

&lt;p&gt;Every detector returns the same kind of region object, and every region is inserted into the same editor as a reviewable overlay.&lt;/p&gt;

&lt;p&gt;That gives the product a stable interaction model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;QR detection can suggest a region&lt;/li&gt;
&lt;li&gt;signature detection can suggest another&lt;/li&gt;
&lt;li&gt;plate detection can suggest blur regions&lt;/li&gt;
&lt;li&gt;the user still reviews all of them the same way&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is much easier to maintain than building a special-case UX for every detector type.&lt;/p&gt;

&lt;h2&gt;
  
  
  The practical lesson
&lt;/h2&gt;

&lt;p&gt;Privacy-sensitive detection gets better when you stop looking for one perfect detector and start using the right signal for each target.&lt;/p&gt;

&lt;p&gt;The useful stack is often not:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;one model&lt;/li&gt;
&lt;li&gt;one answer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;multiple detectors&lt;/li&gt;
&lt;li&gt;narrow heuristics&lt;/li&gt;
&lt;li&gt;normalized region output&lt;/li&gt;
&lt;li&gt;explicit human review before export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That combination is usually much more reliable than a single generalized pass.&lt;/p&gt;

&lt;p&gt;More implementation details:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://happyimg.com/guides/how-to-auto-detect-qr-codes-signatures-and-license-plates-in-the-browser" rel="noopener noreferrer"&gt;https://happyimg.com/guides/how-to-auto-detect-qr-codes-signatures-and-license-plates-in-the-browser&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>privacy</category>
      <category>computervision</category>
    </item>
    <item>
      <title>Building Browser-First Image Redaction Without Uploading Files</title>
      <dc:creator>byeval</dc:creator>
      <pubDate>Wed, 22 Apr 2026 13:50:54 +0000</pubDate>
      <link>https://dev.to/byeval/building-browser-first-image-redaction-without-uploading-files-4mpo</link>
      <guid>https://dev.to/byeval/building-browser-first-image-redaction-without-uploading-files-4mpo</guid>
      <description>&lt;p&gt;If a redaction tool starts by uploading a sensitive screenshot to a server, the product has already created a trust problem.&lt;/p&gt;

&lt;p&gt;That is why I think browser-first redaction is more than a frontend implementation choice. It is part of the product claim.&lt;/p&gt;

&lt;p&gt;The companion guide for this piece is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://happyimg.com/guides/how-browser-first-image-redaction-works-without-uploads" rel="noopener noreferrer"&gt;https://happyimg.com/guides/how-browser-first-image-redaction-works-without-uploads&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What "browser-first" should actually mean
&lt;/h2&gt;

&lt;p&gt;A lot of products say they run in the browser. That statement is too vague to be useful.&lt;/p&gt;

&lt;p&gt;For privacy-sensitive editing, the more meaningful boundary is this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the original image stays local by default&lt;/li&gt;
&lt;li&gt;editing happens on the client&lt;/li&gt;
&lt;li&gt;export is rebuilt locally from the source image&lt;/li&gt;
&lt;li&gt;the final file is downloaded directly in the browser&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That does not make the implementation simpler. It just makes the privacy boundary explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The visible canvas is not the source of truth
&lt;/h2&gt;

&lt;p&gt;One of the first problems in a real editor is coordinate systems.&lt;/p&gt;

&lt;p&gt;Users need a comfortable viewport with zooming and panning. The exported redaction, however, has to map back to the original image dimensions.&lt;/p&gt;

&lt;p&gt;So the visible canvas should be treated as an interaction surface, not as the canonical image.&lt;/p&gt;

&lt;p&gt;The implementation pattern we use is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;keep the original image dimensions as the source of truth&lt;/li&gt;
&lt;li&gt;add the source image as the editor base layer&lt;/li&gt;
&lt;li&gt;fit the viewport to the available screen space&lt;/li&gt;
&lt;li&gt;keep overlays aligned to the original image coordinate system&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That split is what makes browser-side editing and accurate export compatible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Overlays are better than destructive mutations
&lt;/h2&gt;

&lt;p&gt;The next decision was to treat edits as overlay objects rather than immediately mutating the bitmap every time the user interacts with the tool.&lt;/p&gt;

&lt;p&gt;That gives the editor a much better operating model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;redaction boxes can still be moved and resized&lt;/li&gt;
&lt;li&gt;blur and pixelation patches can react to strength changes&lt;/li&gt;
&lt;li&gt;auto-detected regions can be replaced without touching manual edits&lt;/li&gt;
&lt;li&gt;the user can review the exact objects that will affect the export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is especially important in privacy tools because auto-generated suggestions should never feel permanent before review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Export should rebuild the result, not capture the screen
&lt;/h2&gt;

&lt;p&gt;Many browser image tools get loose at export time. They save the current editor state too literally, or effectively take a screenshot of the viewport.&lt;/p&gt;

&lt;p&gt;That is not good enough for a privacy workflow.&lt;/p&gt;

&lt;p&gt;The more reliable pattern is to create a clean export canvas at the original image dimensions, add the source image again, then replay the overlays on top of it.&lt;/p&gt;

&lt;p&gt;In our case that starts with a fresh Fabric static canvas:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exportCanvas&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;StaticCanvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;util&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createCanvasElement&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sourceImageElement&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="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sourceImageElement&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="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then each visible overlay is cloned or reconstructed before generating the final file.&lt;/p&gt;

&lt;p&gt;That matters because the editor may contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;manual redaction shapes&lt;/li&gt;
&lt;li&gt;blur or pixelation effect patches&lt;/li&gt;
&lt;li&gt;auto-detected regions&lt;/li&gt;
&lt;li&gt;text or annotation objects in adjacent tools&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Export should represent the intended result, not whatever happens to be visible on a scaled viewport at that instant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local download is part of the privacy boundary
&lt;/h2&gt;

&lt;p&gt;Once the export exists as a data URL or blob, the browser can download it directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;anchor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createElement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;a&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dataUrl&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;download&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fileName&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;anchor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That seems basic, but in a privacy product it matters. If the editing workflow is local and the export path is local, the product story is easier to understand and easier to trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hard parts are around the edges
&lt;/h2&gt;

&lt;p&gt;Drawing a rectangle on a canvas is not the challenge.&lt;/p&gt;

&lt;p&gt;The real engineering work shows up in the boundaries around the editor:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keeping original coordinates stable while the viewport zooms and pans&lt;/li&gt;
&lt;li&gt;making auto-detection additive instead of destructive&lt;/li&gt;
&lt;li&gt;rebuilding blur and pixelation patches accurately during export&lt;/li&gt;
&lt;li&gt;keeping the editor responsive with large images&lt;/li&gt;
&lt;li&gt;cleaning up canvas resources and workers on teardown&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those are the concerns that determine whether the tool feels credible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Browser-first is a product decision
&lt;/h2&gt;

&lt;p&gt;The main lesson for me was that "runs in the browser" is not the interesting sentence.&lt;/p&gt;

&lt;p&gt;"Keeps sensitive editing local by default" is the interesting sentence.&lt;/p&gt;

&lt;p&gt;That is the real boundary users care about, and it should shape the implementation.&lt;/p&gt;

&lt;p&gt;If a product claims privacy-safe redaction, the architecture should reflect that claim:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;local source handling&lt;/li&gt;
&lt;li&gt;editable overlays&lt;/li&gt;
&lt;li&gt;explicit review&lt;/li&gt;
&lt;li&gt;local export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That will usually earn more trust than adding another server-side processing step and asking users not to worry about it.&lt;/p&gt;

&lt;p&gt;More implementation details:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://happyimg.com/guides/how-browser-first-image-redaction-works-without-uploads" rel="noopener noreferrer"&gt;https://happyimg.com/guides/how-browser-first-image-redaction-works-without-uploads&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>privacy</category>
      <category>frontend</category>
    </item>
    <item>
      <title>OCR Is Not Redaction: Building Safer Auto-Redaction With Tesseract.js</title>
      <dc:creator>byeval</dc:creator>
      <pubDate>Wed, 22 Apr 2026 13:49:57 +0000</pubDate>
      <link>https://dev.to/byeval/ocr-is-not-redaction-building-safer-auto-redaction-with-tesseractjs-1ipj</link>
      <guid>https://dev.to/byeval/ocr-is-not-redaction-building-safer-auto-redaction-with-tesseractjs-1ipj</guid>
      <description>&lt;p&gt;OCR demos usually stop too early.&lt;/p&gt;

&lt;p&gt;They show &lt;code&gt;recognize()&lt;/code&gt;, print some text, and imply that automatic redaction is basically done. In a real product, that is maybe 20 percent of the job.&lt;/p&gt;

&lt;p&gt;What users actually need is a safer pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Run OCR on the image.&lt;/li&gt;
&lt;li&gt;Classify risky spans such as emails, phone numbers, account references, dates, and IDs.&lt;/li&gt;
&lt;li&gt;Map those matched spans back to OCR word boxes.&lt;/li&gt;
&lt;li&gt;Pad the boxes so the text edges are fully covered.&lt;/li&gt;
&lt;li&gt;Insert them as editable regions instead of exporting immediately.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the pattern we use in a browser-first redaction flow built around Tesseract.js.&lt;/p&gt;

&lt;p&gt;The full companion guide is here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://happyimg.com/guides/how-ocr-assisted-redaction-works-with-tesseract-js" rel="noopener noreferrer"&gt;https://happyimg.com/guides/how-ocr-assisted-redaction-works-with-tesseract-js&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why we kept OCR in the browser
&lt;/h2&gt;

&lt;p&gt;Sensitive screenshots are exactly the wrong kind of asset to upload to a server by default just to detect an email address or account number.&lt;/p&gt;

&lt;p&gt;Running OCR in the browser gave us a cleaner privacy boundary:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the image stays local by default&lt;/li&gt;
&lt;li&gt;the user can review the result immediately&lt;/li&gt;
&lt;li&gt;the OCR pass can feed directly into the editor without waiting on a round trip&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That still leaves the hardest part unsolved: turning OCR output into something safe enough to help with redaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Geometry matters more than text
&lt;/h2&gt;

&lt;p&gt;For redaction, plain OCR text is not enough. The editor needs coordinates.&lt;/p&gt;

&lt;p&gt;So instead of treating Tesseract.js as a text extractor, we ask it for structured layout data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;recognize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;asset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ocrSource&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;rotateAuto&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;span class="na"&gt;blocks&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;That gives us paragraphs, lines, and words with bounding boxes. Without those word-level bounds, there are no usable redaction candidates. There is only text.&lt;/p&gt;

&lt;p&gt;We also lazily create and reuse the worker instead of rebuilding it on every scan:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ocrWorkerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createWorker&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;loadOcrWorkerFactory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;ocrWorkerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&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;createWorker&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eng&lt;/span&gt;&lt;span class="dl"&gt;"&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;logger&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ocrWorkerRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setParameters&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;tessedit_pageseg_mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;11&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;preserve_interword_spaces&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;1&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="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That keeps the editor responsive across repeated scans and makes the OCR step feel more like a tool and less like a blocking batch job.&lt;/p&gt;

&lt;h2&gt;
  
  
  The useful trick: match text, then map back to words
&lt;/h2&gt;

&lt;p&gt;The main implementation trick was simple and practical.&lt;/p&gt;

&lt;p&gt;For each OCR line, we rebuild a single line string, but we also keep the character offsets of every OCR word inside that string. That gives us a bridge between pattern matching and image geometry.&lt;/p&gt;

&lt;p&gt;So the flow becomes:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Reconstruct the OCR line as plain text.&lt;/li&gt;
&lt;li&gt;Run regexes for categories like email, phone, URL, date, or ID.&lt;/li&gt;
&lt;li&gt;Find which OCR words overlap each matched character range.&lt;/li&gt;
&lt;li&gt;Merge those word bounds into one redaction region.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That lets us keep the matching logic simple while still ending up with coordinates we can draw and edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tight boxes are risky
&lt;/h2&gt;

&lt;p&gt;One thing that became obvious very quickly: exact glyph bounds look precise in demos, but they are risky in real privacy tooling.&lt;/p&gt;

&lt;p&gt;If the box is too tight, the export can still leak fragments of the text around the edges. So after merging the matched word boxes, we expand the region with padding before inserting it into the editor.&lt;/p&gt;

&lt;p&gt;That padding step ended up being one of the most important product decisions in the whole flow:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;too little padding leaves readable fragments&lt;/li&gt;
&lt;li&gt;too much padding hides useful surrounding context&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So OCR quality alone is not the main issue. Region construction is just as important.&lt;/p&gt;

&lt;h2&gt;
  
  
  OCR should propose, not finalize
&lt;/h2&gt;

&lt;p&gt;This was the biggest product lesson.&lt;/p&gt;

&lt;p&gt;OCR-assisted redaction should not silently modify an image and export the result. It should insert reviewable regions into the editor and let the user confirm, delete, resize, or add more regions before saving.&lt;/p&gt;

&lt;p&gt;For privacy tools, review is not a fallback. It is part of the feature.&lt;/p&gt;

&lt;p&gt;That design also helped with the predictable OCR failure cases:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;low-contrast screenshots&lt;/li&gt;
&lt;li&gt;dense tables with tiny text&lt;/li&gt;
&lt;li&gt;mixed-language content&lt;/li&gt;
&lt;li&gt;broken OCR segmentation&lt;/li&gt;
&lt;li&gt;labels like &lt;code&gt;ID&lt;/code&gt; or &lt;code&gt;Total&lt;/code&gt; that match patterns but are not always sensitive&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once you accept that OCR is a candidate generator instead of a perfect decision-maker, the whole interaction model gets better.&lt;/p&gt;

&lt;h2&gt;
  
  
  The real implementation boundary
&lt;/h2&gt;

&lt;p&gt;Tesseract.js is only the OCR engine. The hard part is the boundary around it.&lt;/p&gt;

&lt;p&gt;What actually made the feature useful was:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;keeping the scan client-side&lt;/li&gt;
&lt;li&gt;reusing the worker efficiently&lt;/li&gt;
&lt;li&gt;preserving stable geometry&lt;/li&gt;
&lt;li&gt;matching only the categories we cared about&lt;/li&gt;
&lt;li&gt;padding regions conservatively&lt;/li&gt;
&lt;li&gt;requiring review before export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the difference between an OCR demo and a privacy tool.&lt;/p&gt;

&lt;p&gt;If you are building something similar, I would strongly recommend optimizing for reviewable suggestions instead of "one-click automatic redaction." The first approach ships. The second usually overpromises.&lt;/p&gt;

&lt;p&gt;More implementation details:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://happyimg.com/guides/how-ocr-assisted-redaction-works-with-tesseract-js" rel="noopener noreferrer"&gt;https://happyimg.com/guides/how-ocr-assisted-redaction-works-with-tesseract-js&lt;/a&gt;&lt;/p&gt;

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