<?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: mno tao</title>
    <description>The latest articles on DEV Community by mno tao (@mno_tao_236ab4649edf4cf9f).</description>
    <link>https://dev.to/mno_tao_236ab4649edf4cf9f</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%2F3999575%2Fb918cde5-f8f4-4711-a921-15447c377a6d.png</url>
      <title>DEV Community: mno tao</title>
      <link>https://dev.to/mno_tao_236ab4649edf4cf9f</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mno_tao_236ab4649edf4cf9f"/>
    <language>en</language>
    <item>
      <title>Prerendering a multilingual Nuxt game catalog without shipping the database</title>
      <dc:creator>mno tao</dc:creator>
      <pubDate>Sun, 06 Sep 2026 01:00:00 +0000</pubDate>
      <link>https://dev.to/mno_tao_236ab4649edf4cf9f/prerendering-a-multilingual-nuxt-game-catalog-without-shipping-the-database-36pf</link>
      <guid>https://dev.to/mno_tao_236ab4649edf4cf9f/prerendering-a-multilingual-nuxt-game-catalog-without-shipping-the-database-36pf</guid>
      <description>&lt;p&gt;A game catalog looks like a client-side application: filters, search, cards, detail pages, and playable iframes. It is still a poor reason to ship the complete content database and ask every crawler—or every phone—to reconstruct the page after JavaScript loads.&lt;/p&gt;

&lt;p&gt;I recently localized a Nuxt catalog with 153 games into four languages. The public result is 676 indexable routes, but the architecture has three strict properties:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;every indexable route is prerendered as complete HTML;&lt;/li&gt;
&lt;li&gt;missing localized content fails the build instead of falling back to English;&lt;/li&gt;
&lt;li&gt;the browser does not receive Nuxt Content’s SQLite/WASM engine or the full game records.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is how those pieces fit together.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the route set explicit
&lt;/h2&gt;

&lt;p&gt;Relying only on a crawler means an accidentally missing link can remove a page from the static build. The catalog already has a source of truth, so use it to produce the complete route list.&lt;/p&gt;

&lt;p&gt;The base set contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;home, popular, new, search, and four site/legal pages;&lt;/li&gt;
&lt;li&gt;one page per category;&lt;/li&gt;
&lt;li&gt;one page per game.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With 153 games and eight categories, that is 169 base routes. A locale mapping applies no prefix to English and &lt;code&gt;/id/&lt;/code&gt;, &lt;code&gt;/it/&lt;/code&gt;, or &lt;code&gt;/pt-br/&lt;/code&gt; to the other languages. The same English slugs remain stable after the prefix.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;169 base routes × 4 locales = 676 indexable routes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Localized 404 pages are also prerendered, but they are &lt;code&gt;noindex&lt;/code&gt; and do not enter the sitemap.&lt;/p&gt;

&lt;p&gt;This list feeds Nitro prerendering and the sitemap generator. The sitemap is not treated as evidence that pages probably exist; a post-build check resolves every &lt;code&gt;&amp;lt;loc&amp;gt;&lt;/code&gt; to a generated HTML file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep one source of truth for product facts
&lt;/h2&gt;

&lt;p&gt;Game titles, iframe URLs, developers, dates, embed types, and stable slugs are product facts. Translators should not rewrite them.&lt;/p&gt;

&lt;p&gt;Descriptions, objectives, controls, tips, category copy, and legal text are localized. A generation step combines those translations with the factual fields and writes the Markdown documents Nuxt Content will consume during the build.&lt;/p&gt;

&lt;p&gt;For each non-English locale, the generator expects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;153 game documents;&lt;/li&gt;
&lt;li&gt;eight category documents;&lt;/li&gt;
&lt;li&gt;four site/legal documents.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is 165 documents per locale and 495 localized Markdown files in total. Missing keys, extra slugs, duplicate entries, or fact drift are build errors.&lt;/p&gt;

&lt;p&gt;The key rule is simple: &lt;strong&gt;an indexable localized route cannot silently borrow English body content&lt;/strong&gt;. A visible fallback is useful in application chrome; it is dangerous when it creates a page advertised to search engines as a different language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Localize the page, not the embedded game
&lt;/h2&gt;

&lt;p&gt;The catalog can translate its navigation, metadata, controls guide, objectives, and warnings. It does not own every embedded game’s UI.&lt;/p&gt;

&lt;p&gt;That boundary should be explicit. The page language changes, while the game iframe may remain in English. Trying to imply otherwise creates misleading metadata and support expectations.&lt;/p&gt;

&lt;p&gt;The language switcher should preserve the current route:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/game/ember-vault/
/it/game/ember-vault/
/pt-br/game/ember-vault/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not redirect based on &lt;code&gt;Accept-Language&lt;/code&gt; or browser settings. Automatic redirects make URLs unstable for crawlers and surprising for people who intentionally chose another language.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate SEO signals from the same locale model
&lt;/h2&gt;

&lt;p&gt;Every page needs a self-referencing canonical and a complete alternate set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;en&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;id-ID&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;it-IT&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pt-BR&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x-default&lt;/code&gt; pointing to English.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The document &lt;code&gt;lang&lt;/code&gt;, Open Graph locale, visible copy, and Schema &lt;code&gt;inLanguage&lt;/code&gt; must agree. The sitemap repeats the same alternates.&lt;/p&gt;

&lt;p&gt;This is a good place for a deterministic verifier. For all 676 HTML files, check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;expected &lt;code&gt;html lang&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;exact canonical URL;&lt;/li&gt;
&lt;li&gt;all four &lt;code&gt;hreflang&lt;/code&gt; entries plus &lt;code&gt;x-default&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;localized Open Graph locale;&lt;/li&gt;
&lt;li&gt;Schema language;&lt;/li&gt;
&lt;li&gt;no unresolved message keys;&lt;/li&gt;
&lt;li&gt;no known English section headings on non-English pages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Counting is surprisingly valuable. If the expected route count is 676 and the verifier saw 675, the build fails before deployment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Split card data from detail data
&lt;/h2&gt;

&lt;p&gt;The full game record contains fields that a grid never needs: iframe URL, long controls, features, developer information, and related-game details. Importing that object into a shared composable can put the entire catalog into a client chunk.&lt;/p&gt;

&lt;p&gt;A build step projects a slim card record containing only fields used by search and grids, such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;slug, title, description, category, tags,
thumbnail, embed type, difficulty, updated date, popular
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Detail fields stay server/build-side. The detail route receives one game during prerendering and serializes only what that page needs. Legal and editorial pages do not preload the card catalog merely because they share a layout.&lt;/p&gt;

&lt;p&gt;Add a leak test with a known detail-only iframe URL. If that marker appears in a shared JavaScript chunk, the split has regressed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remove the client database when all queries are prerendered
&lt;/h2&gt;

&lt;p&gt;Nuxt Content can ship a SQLite/WASM client for browser-side queries. A fully static catalog does not need it if every content query runs during prerender and the result is hydrated from the page payload.&lt;/p&gt;

&lt;p&gt;The production build aliases the client database module to a stub that must never execute. Post-build checks then fail if they find:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SQLite WASM or worker artifacts;&lt;/li&gt;
&lt;li&gt;public content database dumps;&lt;/li&gt;
&lt;li&gt;a runtime fetch for the full catalog;&lt;/li&gt;
&lt;li&gt;old code that overwrites the prerendered description after hydration.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last item protects more than performance. Runtime replacement can show one description without JavaScript and a shorter, different one with JavaScript, which makes accessibility and indexing unpredictable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put budgets around the remaining JavaScript
&lt;/h2&gt;

&lt;p&gt;Internationalization has a real bundle cost. Lazy locale dictionaries help, but only if the application does not preload all languages on every route.&lt;/p&gt;

&lt;p&gt;Track both the largest chunk and total emitted JavaScript. Also scan locale chunks for markers from more than one language. A budget is not a universal performance score; it is a tripwire against accidental catalog or dictionary duplication.&lt;/p&gt;

&lt;p&gt;The browser check should visit a localized game page directly, before navigating from English. That catches implementations that work only after a locale bundle has already been loaded.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the game boundary too
&lt;/h2&gt;

&lt;p&gt;A perfectly localized wrapper is still broken if the play button cannot launch its iframe. For a small set of representative self-hosted games, browser checks should:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;open the localized detail route;&lt;/li&gt;
&lt;li&gt;verify localized initial HTML;&lt;/li&gt;
&lt;li&gt;launch the iframe;&lt;/li&gt;
&lt;li&gt;send a real keyboard or touch input;&lt;/li&gt;
&lt;li&gt;observe game progress rather than elapsed time;&lt;/li&gt;
&lt;li&gt;verify pause, restart, focus, and mobile sizing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For third-party embeds, record that availability is an external dependency. Do not translate or bundle their binaries as if they were catalog content.&lt;/p&gt;

&lt;p&gt;The reference implementation discussed here is &lt;a href="https://sonotap.online/" rel="noopener noreferrer"&gt;SonoTap&lt;/a&gt;. The transferable pattern is to use static generation as an integrity boundary: routes, translations, metadata, content, and client payloads are all artifacts that a build can count and reject.&lt;/p&gt;

</description>
      <category>seo</category>
      <category>webdev</category>
      <category>nuxt</category>
      <category>i18n</category>
    </item>
    <item>
      <title>What “local OCR” should mean in a web application</title>
      <dc:creator>mno tao</dc:creator>
      <pubDate>Sat, 05 Sep 2026 01:00:00 +0000</pubDate>
      <link>https://dev.to/mno_tao_236ab4649edf4cf9f/what-local-ocr-should-mean-in-a-web-application-1ai7</link>
      <guid>https://dev.to/mno_tao_236ab4649edf4cf9f/what-local-ocr-should-mean-in-a-web-application-1ai7</guid>
      <description>&lt;p&gt;“Runs in your browser” is easy to put on a landing page. It is harder to make it a property of the whole OCR pipeline.&lt;/p&gt;

&lt;p&gt;An OCR page can have a static interface and still send the selected file to an API. It can keep the image local but put the extracted text into error reporting. It can avoid both and still leak a protected PDF password through a URL or form submission. If privacy is part of the product, the useful question is not where the JavaScript was downloaded from. It is: &lt;strong&gt;which user-derived bytes can cross a process or network boundary, and why?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is the approach I use for a browser-only OCR tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a small threat model
&lt;/h2&gt;

&lt;p&gt;For an image or PDF OCR session, treat all of these as sensitive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source bytes;&lt;/li&gt;
&lt;li&gt;filenames and MIME metadata;&lt;/li&gt;
&lt;li&gt;PDF passwords;&lt;/li&gt;
&lt;li&gt;rendered pages and crops;&lt;/li&gt;
&lt;li&gt;recognized text and table output;&lt;/li&gt;
&lt;li&gt;parser and model errors that could contain user-derived values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The default rule is that none of them may be placed in &lt;code&gt;fetch&lt;/code&gt;, forms, beacons, analytics payloads, URLs, browser storage, or logs. Downloading application code, OCR models, and a public character dictionary is different: those resources are the same for every user and contain no document data.&lt;/p&gt;

&lt;p&gt;That distinction makes the network policy testable. A test can select a uniquely marked fixture, perform OCR, and fail if the marker or source bytes appear in any outgoing request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Move inference off the main thread
&lt;/h2&gt;

&lt;p&gt;OCR is both CPU- and memory-heavy. Running it on the UI thread creates frozen progress indicators and tempts developers to move the whole job to a server.&lt;/p&gt;

&lt;p&gt;A dedicated Web Worker gives a better local architecture:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The page decodes a selected file into an &lt;code&gt;ImageBitmap&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Ownership of the bitmap is transferred to the worker.&lt;/li&gt;
&lt;li&gt;The worker loads ONNX Runtime Web and the selected detection and recognition models.&lt;/li&gt;
&lt;li&gt;It reports coarse progress states such as model loading, detection, and recognition.&lt;/li&gt;
&lt;li&gt;It returns structured lines and then closes the bitmap.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Transferable objects matter here. Copying a large RGBA buffer between the main thread and a worker can briefly double its memory cost. Transferring an &lt;code&gt;ImageBitmap&lt;/code&gt; avoids that copy, while explicitly closing it keeps repeated batches from accumulating GPU-backed resources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put bounds before decoding
&lt;/h2&gt;

&lt;p&gt;File size is not enough to predict decode cost. A compressed image can expand into tens or hundreds of megabytes of pixels. A practical pipeline checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;accepted MIME types and extensions;&lt;/li&gt;
&lt;li&gt;compressed byte size;&lt;/li&gt;
&lt;li&gt;decoded width, height, and pixel count;&lt;/li&gt;
&lt;li&gt;a maximum working dimension for inference;&lt;/li&gt;
&lt;li&gt;a separate hard ceiling for safe decoding.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For formats with no universal browser decoder, isolate the decoder. HEIC can run in a short-lived worker; TIFF and BMP can have explicit dimension and buffer checks. After decoding, constrain the bitmap before inference. The OCR detector rarely needs a full 12-megapixel phone photo to find lines of text.&lt;/p&gt;

&lt;p&gt;The important failure behavior is deterministic: reject an unsafe image before allocating another full-size buffer, return a localized error, and release anything already created.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate text detection from recognition
&lt;/h2&gt;

&lt;p&gt;A useful local OCR pipeline is not one giant black box. Mine has two model stages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a detector produces a text probability map;&lt;/li&gt;
&lt;li&gt;connected regions become candidate boxes and nearby boxes are merged;&lt;/li&gt;
&lt;li&gt;each crop is deskewed into a normalized canvas;&lt;/li&gt;
&lt;li&gt;a recognition model produces character probabilities;&lt;/li&gt;
&lt;li&gt;CTC decoding turns those probabilities into text and confidence values.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The detector can operate on a bounded, resized image. Recognition still maps boxes back to the source bitmap, so each line gets a cleaner crop. This also makes table-oriented output possible: the UI retains text plus geometry instead of receiving one opaque string.&lt;/p&gt;

&lt;p&gt;Two recognition models are a reasonable product tradeoff. A small model reduces first-use download and latency; a larger model can be loaded only when the user asks for higher accuracy. “High accuracy” should describe the relative model choice, not promise perfect OCR.&lt;/p&gt;

&lt;h2&gt;
  
  
  PDFs need their own boundary
&lt;/h2&gt;

&lt;p&gt;A PDF workflow is not just an image workflow with a different file extension. It introduces page ranges, passwords, malformed structures, and potentially hundreds of render operations.&lt;/p&gt;

&lt;p&gt;Render only selected pages, cap the document size and page count, and keep the password in memory. Each page should be converted to a bounded bitmap and passed through the same worker pipeline. Release page canvases as soon as their OCR result is complete.&lt;/p&gt;

&lt;p&gt;If a user can pause or stop a batch, make that a real state transition. It should stop scheduling new pages, not merely hide a spinner while work continues in the background.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be precise about offline support
&lt;/h2&gt;

&lt;p&gt;“Works offline” does not mean the first visit works without a network. The application shell and a chosen OCR model have to be downloaded first.&lt;/p&gt;

&lt;p&gt;A service worker can cache immutable application resources and models, but model versions need explicit names. When a model changes, old and new caches should not be confused. The UI should say that models load on demand and that offline reuse begins after caching.&lt;/p&gt;

&lt;p&gt;Test offline behavior after a successful warm-up, then reload with the network disabled. Also test a model the user has not cached: that failure should be clear rather than silently switching to an unrelated model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Translate operational text, not just the heading
&lt;/h2&gt;

&lt;p&gt;OCR interfaces contain many strings that appear only during work: decode failures, password prompts, page progress, model downloads, pause states, empty results, and export errors. A translated landing page with English worker errors is not a translated product.&lt;/p&gt;

&lt;p&gt;One approach is to keep worker errors as stable codes and translate them in the UI. If a worker must produce fallback text, verify every public error path in every locale. Missing messages should fail the build rather than fall back silently on an indexable page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests that support the privacy claim
&lt;/h2&gt;

&lt;p&gt;The highest-value tests are not snapshots of the hero section. I keep checks for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;recognized text from a repository-authored public fixture;&lt;/li&gt;
&lt;li&gt;Fast and higher-accuracy model selection;&lt;/li&gt;
&lt;li&gt;a PDF page range and a password-protected PDF;&lt;/li&gt;
&lt;li&gt;stop, continue, clear, and batch export states;&lt;/li&gt;
&lt;li&gt;oversized and malformed input;&lt;/li&gt;
&lt;li&gt;zero user-derived network requests;&lt;/li&gt;
&lt;li&gt;offline reload after model caching;&lt;/li&gt;
&lt;li&gt;memory cleanup after replacing or clearing files;&lt;/li&gt;
&lt;li&gt;the same workflow in every interface language.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No browser test proves that all future code will be private. It does make a privacy regression visible in the same place as a broken download button.&lt;/p&gt;

&lt;p&gt;The reference implementation behind these notes is &lt;a href="https://browserocr.com/" rel="noopener noreferrer"&gt;BrowserOCR&lt;/a&gt;. The useful idea is broader than one product: local processing is an end-to-end constraint, not a badge attached to a client-side upload form.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>machinelearning</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Safely parsing email files in the browser</title>
      <dc:creator>mno tao</dc:creator>
      <pubDate>Fri, 04 Sep 2026 00:40:06 +0000</pubDate>
      <link>https://dev.to/mno_tao_236ab4649edf4cf9f/safely-parsing-email-files-in-the-browser-3j7b</link>
      <guid>https://dev.to/mno_tao_236ab4649edf4cf9f/safely-parsing-email-files-in-the-browser-3j7b</guid>
      <description>&lt;p&gt;An email file is not just text plus a few attachments. It can contain HTML, nested MIME parts, misleading filenames, inline resources, remote tracking pixels, malformed encodings, and enough data to exhaust a browser tab.&lt;/p&gt;

&lt;p&gt;Moving parsing into the browser removes an upload from the architecture, but it does not automatically make the viewer safe. It changes the security job: untrusted content is now being interpreted next to the user’s active web session.&lt;/p&gt;

&lt;p&gt;This is the checklist I use for a local EML and winmail.dat/TNEF reader.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat every parsed field as untrusted
&lt;/h2&gt;

&lt;p&gt;The sender, subject, recipient, filename, MIME type, and message body all came from a file. Render headers and filenames as text, never by concatenating HTML.&lt;/p&gt;

&lt;p&gt;The same applies to errors. A parser exception can include a filename or fragment of malformed input. Showing that message verbatim may leak data into logs or turn it into markup. Map parser failures to stable error categories, then display a controlled explanation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalize into one internal model
&lt;/h2&gt;

&lt;p&gt;EML and TNEF have different container structures, but the UI should not contain two independent security implementations.&lt;/p&gt;

&lt;p&gt;Both parsers can produce a common message model:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;subject, sender, to, cc, date,
plain body, sanitized HTML candidate,
attachments[] {
  safe filename, MIME type, bytes,
  inline flag, content ID, content location
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The normalization layer is the right place to enforce per-source limits and reject unsupported structures. The viewer and download code then work against the same constrained data regardless of input format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sanitize HTML as hostile input
&lt;/h2&gt;

&lt;p&gt;Email HTML was designed for mail clients, not for direct insertion into an application DOM.&lt;/p&gt;

&lt;p&gt;A conservative policy removes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;scripts and event handlers;&lt;/li&gt;
&lt;li&gt;forms and interactive controls;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;iframe&lt;/code&gt;, &lt;code&gt;object&lt;/code&gt;, and &lt;code&gt;embed&lt;/code&gt; elements;&lt;/li&gt;
&lt;li&gt;styles and CSS URLs;&lt;/li&gt;
&lt;li&gt;unsafe protocols;&lt;/li&gt;
&lt;li&gt;executable or unexpected embedded content.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use a maintained sanitizer with a pinned version, but do not stop at its default configuration. Email has resource-loading behavior that a generic “safe HTML” preset may still allow.&lt;/p&gt;

&lt;p&gt;Plain text should remain the fallback. If HTML cannot be sanitized into the allowed subset, showing text is better than trying to preserve every visual detail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rewrite local inline images
&lt;/h2&gt;

&lt;p&gt;Legitimate messages often reference an attachment using &lt;code&gt;cid:&lt;/code&gt; or &lt;code&gt;Content-Location&lt;/code&gt;. Those images can be displayed without a network request:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;match the reference to a parsed attachment;&lt;/li&gt;
&lt;li&gt;create a Blob from the attachment bytes;&lt;/li&gt;
&lt;li&gt;create an object URL;&lt;/li&gt;
&lt;li&gt;replace the resource reference with that local URL;&lt;/li&gt;
&lt;li&gt;revoke the URL when the message is switched, removed, cleared, or the page exits.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Matching needs normalized identifiers and explicit MIME checks. Do not let an attachment’s declared filename or content location become an arbitrary URL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep remote images out of the DOM by default
&lt;/h2&gt;

&lt;p&gt;Removing a visible remote image after rendering is too late. The request may already have exposed the user’s IP address, time, user agent, and a unique tracking token.&lt;/p&gt;

&lt;p&gt;The safer sequence is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;parse and sanitize the body;&lt;/li&gt;
&lt;li&gt;replace remote image sources with inert placeholders before insertion;&lt;/li&gt;
&lt;li&gt;tell the user that external images are blocked;&lt;/li&gt;
&lt;li&gt;only after an explicit action for the current message, restore allowed HTTPS sources;&lt;/li&gt;
&lt;li&gt;use a no-referrer policy.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;“For the current message” is important. Permission should not silently carry to another email in the batch, and it does not belong in local storage.&lt;/p&gt;

&lt;p&gt;A browser test should observe the network and assert zero remote-image requests before the click. After the click, it should allow only the expected image request and no navigation or script execution.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make downloads path-safe
&lt;/h2&gt;

&lt;p&gt;Attachment names can contain path separators, control characters, reserved device names, or repeated values. Normalize every name before using it in a download or ZIP archive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;strip directory components;&lt;/li&gt;
&lt;li&gt;remove control and unsafe characters;&lt;/li&gt;
&lt;li&gt;provide a fallback when the result is empty;&lt;/li&gt;
&lt;li&gt;cap length;&lt;/li&gt;
&lt;li&gt;deduplicate names deterministically.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;ZIP creation can happen entirely in memory. Object URLs used for individual files, PDF previews, and archives should have a clear owner and lifecycle. Revoke too early and downloads fail; never revoke and repeated batches leak memory.&lt;/p&gt;

&lt;p&gt;PDF attachments deserve another small boundary. Create a preview only after the user asks, and use a Blob URL. Do not embed an untrusted remote URL just because an attachment declared itself as a PDF.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put limits on expansion, not only input bytes
&lt;/h2&gt;

&lt;p&gt;A 25 MB source can contain many MIME parts or highly compressible attachments. Useful limits include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source files per batch;&lt;/li&gt;
&lt;li&gt;bytes per source and per batch;&lt;/li&gt;
&lt;li&gt;MIME parts or attachments per source;&lt;/li&gt;
&lt;li&gt;total successfully extracted attachments;&lt;/li&gt;
&lt;li&gt;nesting depth and parser work, when the library exposes them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check limits during parsing instead of building an unbounded structure and rejecting it afterward.&lt;/p&gt;

&lt;p&gt;The UI should distinguish “unsupported format,” “malformed message,” and “safe limit exceeded.” That helps users without exposing parser internals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local processing includes telemetry
&lt;/h2&gt;

&lt;p&gt;A page can parse locally and still send sensitive metadata to analytics or error reporting. For a mail viewer, do not collect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;filenames or file sizes;&lt;/li&gt;
&lt;li&gt;subject, sender, recipient, or body;&lt;/li&gt;
&lt;li&gt;attachment names, types, or counts derived from one message;&lt;/li&gt;
&lt;li&gt;parser errors containing input values;&lt;/li&gt;
&lt;li&gt;full referrers or query strings that could carry user data.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If aggregate task events exist, make their vocabulary fixed and inspect the exact payload. A successful open can be counted without serializing anything about the email that was opened.&lt;/p&gt;

&lt;p&gt;Also avoid storing parsed messages in &lt;code&gt;localStorage&lt;/code&gt;, &lt;code&gt;sessionStorage&lt;/code&gt;, IndexedDB, cookies, or URLs. “The files never leave your device” is weaker than “the message stays in memory for this session.” State which one you mean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test with synthetic fixtures
&lt;/h2&gt;

&lt;p&gt;Real user mail is a tempting source of edge cases and a bad test asset. Build repository-owned fixtures containing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;plain text and sanitized HTML alternatives;&lt;/li&gt;
&lt;li&gt;CID and Content-Location images;&lt;/li&gt;
&lt;li&gt;one deliberately blocked HTTPS image;&lt;/li&gt;
&lt;li&gt;duplicate and path-like filenames;&lt;/li&gt;
&lt;li&gt;zero, one, and many attachments;&lt;/li&gt;
&lt;li&gt;malformed input and exact size/count boundaries;&lt;/li&gt;
&lt;li&gt;Unicode headers and filenames.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use unique harmless markers so the test can detect accidental requests or persistence. Run the same fixture through every translated interface; security warnings and errors are part of the product, not incidental copy.&lt;/p&gt;

&lt;p&gt;The reference implementation for this checklist is &lt;a href="https://mailfileviewer.com/" rel="noopener noreferrer"&gt;Mail File Viewer&lt;/a&gt;. Browser-only parsing reduces one major exposure, but the remaining work—sanitization, resource policy, bounds, and cleanup—is what makes the result defensible.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>privacy</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>Hitting an image size ceiling with Rust and WebAssembly</title>
      <dc:creator>mno tao</dc:creator>
      <pubDate>Thu, 03 Sep 2026 08:12:16 +0000</pubDate>
      <link>https://dev.to/mno_tao_236ab4649edf4cf9f/hitting-an-image-size-ceiling-with-rust-and-webassembly-2e4m</link>
      <guid>https://dev.to/mno_tao_236ab4649edf4cf9f/hitting-an-image-size-ceiling-with-rust-and-webassembly-2e4m</guid>
      <description>&lt;p&gt;Many image tools expose a quality slider. Forms and government portals usually expose a different requirement: “the file must be under 100 KB.”&lt;/p&gt;

&lt;p&gt;Those are not the same problem. A quality value is an encoder input. A byte ceiling is a constraint on the output, and the relationship between the two depends on the pixels, dimensions, color profile, output format, and encoder.&lt;/p&gt;

&lt;p&gt;I wanted a HEIC converter with one simple invariant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;output_bytes &amp;lt;= target_kb * 1024
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The implementation runs in the browser, with Rust compiled to WebAssembly. Here are the design choices that mattered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decode once and keep a bounded image object
&lt;/h2&gt;

&lt;p&gt;HEIC decoding is expensive enough that repeating it for preview, rotation, compression, and conversion is wasteful. The WebAssembly boundary therefore exposes an image object that owns one decoded RGB buffer.&lt;/p&gt;

&lt;p&gt;The browser creates the object from the HEIC bytes, requests a JPEG preview, changes rotation if needed, and then calls either &lt;code&gt;compress&lt;/code&gt; or &lt;code&gt;convert&lt;/code&gt;. Replacing the input explicitly frees the old object.&lt;/p&gt;

&lt;p&gt;The decoder has pixel and memory limits before it returns that buffer. This is not optional in a browser tab: a small compressed file can decode into a very large allocation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define “precise size” as a ceiling
&lt;/h2&gt;

&lt;p&gt;An encoder cannot promise an output of exactly 100,000 bytes without padding or a more complicated rate-control system. Padding has no user value, and a 99 KB file satisfies a 100 KB form.&lt;/p&gt;

&lt;p&gt;So the product contract is “at or below the selected target.” The UI reports the actual result. Tests assert the byte inequality, not a rounded label.&lt;/p&gt;

&lt;p&gt;That also avoids the KB ambiguity hiding in the interface. Internally, the target is converted once to bytes and every comparison uses that value.&lt;/p&gt;

&lt;h2&gt;
  
  
  JPEG: search quality before dimensions
&lt;/h2&gt;

&lt;p&gt;For JPEG, encoded size is sufficiently ordered by quality to make binary search practical. The algorithm searches a protected quality interval rather than the entire 1–100 range:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Q_MIN = 55
search [55, 100] for the highest quality whose bytes fit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The minimum is a product decision. Very low JPEG quality can technically hit a target while producing obvious blocks around faces and text. Below the floor, reducing dimensions usually gives a more usable result.&lt;/p&gt;

&lt;p&gt;There is a useful optimization for tight targets: encode at &lt;code&gt;Q_MIN&lt;/code&gt; first. If even that output is too large, the other quality probes at the same dimensions are doomed. Skip them and resize.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;current_bytes&lt;/code&gt; is the failed encoding, estimate the next scale from image area:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;scale = sqrt(target_bytes / current_bytes) * 0.95
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The square root appears because both width and height change. The 0.95 factor leaves a small margin so the next attempt is less likely to miss by a few bytes. Resize from the original decoded pixels with a high-quality Lanczos filter, then repeat the quality search.&lt;/p&gt;

&lt;p&gt;The result is the highest JPEG quality at the largest dimensions found under the ceiling, subject to the quality floor and iteration limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  PNG is a different algorithm
&lt;/h2&gt;

&lt;p&gt;Lossless PNG has no quality knob comparable to JPEG. Compression level and filter choices do not produce a clean monotonic control that can be binary-searched against output size.&lt;/p&gt;

&lt;p&gt;For PNG, dimensions are the practical lever:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;estimate an initial downscale for very large photos, avoiding a slow full-resolution PNG encode;&lt;/li&gt;
&lt;li&gt;encode the resized pixels and compare real bytes;&lt;/li&gt;
&lt;li&gt;continue scaling down until the output fits;&lt;/li&gt;
&lt;li&gt;try a few “grow-back” steps from the original pixels to use more of the remaining byte budget.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The grow-back step matters. A conservative scale can produce a 92 KB image for a 100 KB target. Increasing both dimensions slightly may retain more detail and still fit. Each candidate is resized from the original RGB buffer, not from the already reduced image, to avoid accumulating blur.&lt;/p&gt;

&lt;p&gt;JPEG does not need this phase because quality search already spends the available budget at a fixed dimension.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep conversion separate from compression
&lt;/h2&gt;

&lt;p&gt;“Convert HEIC to JPEG” and “compress HEIC below 100 KB” are different user intents.&lt;/p&gt;

&lt;p&gt;The conversion path uses the original pixel dimensions and a fixed high JPEG quality, or lossless PNG. It does not run the target-size loop. The output can be larger than the HEIC input, especially for PNG, and the interface should say so.&lt;/p&gt;

&lt;p&gt;Mixing these paths creates surprising tools that label an operation “convert” while quietly shrinking a photo to meet an arbitrary size.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve color information when possible
&lt;/h2&gt;

&lt;p&gt;A technically correct byte result can still look wrong if its color profile disappears. The decoder extracts an ICC profile when available, and both the preview and final encoder receive it.&lt;/p&gt;

&lt;p&gt;Using the same profile for the “before” preview and output also prevents a misleading comparison where only one side is color-managed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the WebAssembly contract boring
&lt;/h2&gt;

&lt;p&gt;The JavaScript-facing result contains bytes plus explicit metadata:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;output, MIME type, extension,
input width/height, output width/height,
JPEG quality or null for PNG,
encoding passes, resize passes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That shape keeps UI code out of the compression algorithm. The page creates object URLs for preview and download, revokes them when the file changes, and never sends source or result bytes to a server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the invariant with real HEIC files
&lt;/h2&gt;

&lt;p&gt;Synthetic RGB patterns are useful for forcing difficult compression, but they skip the HEIC decoder—the most format-specific part of the pipeline.&lt;/p&gt;

&lt;p&gt;The test suite includes a repository-owned phone photo and checks several targets for both JPEG and PNG. Useful assertions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;output bytes never exceed the target;&lt;/li&gt;
&lt;li&gt;JPEG quality never falls below the floor;&lt;/li&gt;
&lt;li&gt;output dimensions are positive and preserve orientation;&lt;/li&gt;
&lt;li&gt;conversion retains the original dimensions;&lt;/li&gt;
&lt;li&gt;ICC data survives when the fixture contains it;&lt;/li&gt;
&lt;li&gt;encoding and resize pass counts remain bounded;&lt;/li&gt;
&lt;li&gt;the browser can download and decode the result;&lt;/li&gt;
&lt;li&gt;no image-derived request leaves the page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The reference implementation is open source as &lt;a href="https://heictosize.com/" rel="noopener noreferrer"&gt;HEIC To Size&lt;/a&gt; under AGPL-3.0-only. The larger lesson is that a byte target should be modeled as an invariant with format-specific control variables, not as a renamed quality slider.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>webdev</category>
      <category>webassembly</category>
      <category>imageprocessing</category>
    </item>
  </channel>
</rss>
