<?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: El_Necora</title>
    <description>The latest articles on DEV Community by El_Necora (@robertcasschdot).</description>
    <link>https://dev.to/robertcasschdot</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%2F4002115%2Fdf753015-bb13-472f-8c83-9c303cb04976.png</url>
      <title>DEV Community: El_Necora</title>
      <link>https://dev.to/robertcasschdot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/robertcasschdot"/>
    <language>en</language>
    <item>
      <title>What you can (and cannot) do to a PDF entirely in the browser</title>
      <dc:creator>El_Necora</dc:creator>
      <pubDate>Thu, 06 Aug 2026 11:06:15 +0000</pubDate>
      <link>https://dev.to/robertcasschdot/what-you-can-and-cannot-do-to-a-pdf-entirely-in-the-browser-3lmn</link>
      <guid>https://dev.to/robertcasschdot/what-you-can-and-cannot-do-to-a-pdf-entirely-in-the-browser-3lmn</guid>
      <description>&lt;p&gt;Every "free PDF tool" site works the same way: you upload your file, a server does the work, you download the result. Which is fine for a restaurant menu and considerably less fine for a signed contract, a payslip, or a scan of your passport.&lt;/p&gt;

&lt;p&gt;A friend and I wanted to know how much of that could just happen locally. Turns out: most of it. But the boundary is in a place I didn't expect, and a couple of the "obvious" implementations are quietly wrong in ways that matter.&lt;/p&gt;

&lt;p&gt;Here's the map, based on building about 27 of these.&lt;/p&gt;

&lt;h2&gt;
  
  
  The easy tier: structural edits
&lt;/h2&gt;

&lt;p&gt;Merging, splitting, rotating, reordering pages, adding page numbers — none of this needs you to &lt;em&gt;render&lt;/em&gt; anything. A PDF is a document object model. &lt;code&gt;pdf-lib&lt;/code&gt; lets you open it, move objects around, and write it back out.&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;const&lt;/span&gt; &lt;span class="nx"&gt;merged&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;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&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;const&lt;/span&gt; &lt;span class="nx"&gt;bytes&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;files&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;src&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;PDFDocument&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bytes&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;pages&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;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;copyPages&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getPageIndices&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="nx"&gt;pages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addPage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;p&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;out&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;merged&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole merge tool. It runs in milliseconds and there is genuinely no reason for this to ever touch a server. The fact that it usually does is a business decision, not a technical one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The middle tier: you need pixels
&lt;/h2&gt;

&lt;p&gt;Converting to JPG, compressing, extracting images — now you need &lt;code&gt;pdf.js&lt;/code&gt; to actually rasterize pages onto a canvas. Slower, heavier, but still entirely local.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap: redaction
&lt;/h2&gt;

&lt;p&gt;This one deserves its own section because getting it wrong is a real problem, and plenty of tools do get it wrong.&lt;/p&gt;

&lt;p&gt;The intuitive implementation of "redact this paragraph" is: draw a black rectangle over it. It looks perfect. The PDF opens with a black bar exactly where the sensitive text was.&lt;/p&gt;

&lt;p&gt;The text is still there.&lt;/p&gt;

&lt;p&gt;PDFs keep content in layers. A rectangle drawn on top is a new object; the text object underneath is untouched. Select-all and copy, or run any text extractor, and the "redacted" content comes straight out. This has burned actual law firms and government agencies — there's a genuine history of court filings being un-redacted by journalists with Ctrl+A.&lt;/p&gt;

&lt;p&gt;Doing it properly means destroying the information, not covering it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Render the page to a canvas with &lt;code&gt;pdf.js&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Fill the redaction rectangles black &lt;strong&gt;on the canvas&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Build a new PDF from the resulting raster image&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You lose the text layer for that page — the output is a picture of a document rather than a document. That is not a side effect to apologise for, it's the entire point. If the text is still selectable, you didn't redact anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotcha: pdf-lib can't encrypt
&lt;/h2&gt;

&lt;p&gt;I wanted a "password protect this PDF" tool. &lt;code&gt;pdf-lib&lt;/code&gt; doesn't do encryption. Not a flag I missed — it isn't implemented.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;@cantoo/pdf-lib&lt;/code&gt; is a fork that adds it, with the same API:&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;await&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;userPassword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ownerPassword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pw&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The reverse direction is more interesting. PDFs have two kinds of password:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Owner password&lt;/strong&gt; — restrictions on printing, copying, editing. This is a flag the viewer is politely asked to respect. Load with &lt;code&gt;ignoreEncryption: true&lt;/code&gt;, save again, restrictions gone. It was never security.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User password&lt;/strong&gt; — actual encryption. The file content is genuinely encrypted and without the password there is nothing to recover.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the unlock tool removes the first and cannot touch the second, and the FAQ says exactly that. I'd rather explain the limit than have someone assume we're a cracking service.&lt;/p&gt;

&lt;h2&gt;
  
  
  "Editing" a PDF, honestly
&lt;/h2&gt;

&lt;p&gt;The flagship tool is editing, and I want to be precise about what it does, because most tools in this space are vague on purpose.&lt;/p&gt;

&lt;p&gt;You are not editing the original text stream. Reflowing text inside an existing PDF means reconstructing font metrics, kerning and line breaking for embedded subsets — that's a research project, not a weekend feature.&lt;/p&gt;

&lt;p&gt;What you can do, and what actually solves the problem people have, is overlay: white-out box over the old text, new text on top, plus drawing, images and signatures. Export flattens it all with &lt;code&gt;pdf-lib&lt;/code&gt;. From the user's side it reads as editing. It also works on scans, where there is no text layer at all.&lt;/p&gt;

&lt;p&gt;The one thing that will eat an afternoon is coordinates. The browser puts the origin top-left, PDF points put it bottom-left, and your canvas is at some display scale that isn't 1:1:&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;const&lt;/span&gt; &lt;span class="nx"&gt;pdfX&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cssX&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;scale&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;pdfY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;pageHeight&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cssY&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;scale&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;elementHeight&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Get that wrong and everything lands mirrored down the page, which is a genuinely funny bug the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  OCR, and being consistent about it
&lt;/h2&gt;

&lt;p&gt;For scanned documents you need OCR, and &lt;code&gt;tesseract.js&lt;/code&gt; is excellent. By default it fetches its worker, its WASM core and the language data — about 24MB — from a CDN at runtime.&lt;/p&gt;

&lt;p&gt;Which would have meant a site whose entire premise is "your files never leave your browser" reaching out to a third party the moment you use it. Not the same thing as uploading your file, but not a distinction I wanted to have to explain either.&lt;/p&gt;

&lt;p&gt;So it's all self-hosted:&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;const&lt;/span&gt; &lt;span class="nx"&gt;worker&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="nx"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;OEM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;LSTM_ONLY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;corePath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tesseract/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;langPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/tesseract/lang&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Bigger deploy, zero external calls. If you're making a privacy claim, it should survive someone opening the network tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  What genuinely doesn't work well
&lt;/h2&gt;

&lt;p&gt;PDF to Word. You can pull the text out with &lt;code&gt;pdf.js&lt;/code&gt; and group it into lines by Y coordinate, and we ship that — but layout reconstruction, tables and styling are not happening client-side at any quality worth defending. The tool says so up front rather than producing a mess and letting you find out.&lt;/p&gt;

&lt;p&gt;PowerPoint we skipped entirely. Bad effort-to-value ratio.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it landed
&lt;/h2&gt;

&lt;p&gt;27 tools, all client-side, free, no watermark, no sign-up, no upload. Built with Astro, deployed as a static site. English and Spanish.&lt;/p&gt;

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

&lt;p&gt;The stack is basically &lt;code&gt;pdf-lib&lt;/code&gt; / &lt;code&gt;@cantoo/pdf-lib&lt;/code&gt; for structure, &lt;code&gt;pdf.js&lt;/code&gt; for rendering, &lt;code&gt;tesseract.js&lt;/code&gt; for OCR, &lt;code&gt;mammoth&lt;/code&gt; and &lt;code&gt;docx&lt;/code&gt; for Word, SheetJS for spreadsheets. All of it has been sitting in npm for years. Nobody needed to build the upload-a-server version of this in the first place.&lt;/p&gt;

&lt;p&gt;If you're doing something similar and hit a case where the browser genuinely can't cut it, I'd like to hear which one.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>showdev</category>
      <category>privacy</category>
    </item>
    <item>
      <title>Compiling libjxl to WASM so browsers can open JPEG XL files</title>
      <dc:creator>El_Necora</dc:creator>
      <pubDate>Thu, 06 Aug 2026 10:46:12 +0000</pubDate>
      <link>https://dev.to/robertcasschdot/compiling-libjxl-to-wasm-so-browsers-can-open-jpeg-xl-files-3gjp</link>
      <guid>https://dev.to/robertcasschdot/compiling-libjxl-to-wasm-so-browsers-can-open-jpeg-xl-files-3gjp</guid>
      <description>&lt;p&gt;Chrome shipped JPEG XL behind a flag in 2021, then pulled it in 2022. Firefox has it in Nightly only. Safari supports it. So depending on which browser you happen to have open, a &lt;code&gt;.jxl&lt;/code&gt; file is either an image or a file your OS shrugs at.&lt;/p&gt;

&lt;p&gt;That's an annoying place for a format to be, because people do end up with these files. Some camera and phone export pipelines produce them, and anyone who has gone down the "let me re-encode my photo library" road has a folder of them somewhere. Then they try to open one on a machine that isn't a Mac and nothing happens.&lt;/p&gt;

&lt;p&gt;I wanted a viewer that runs entirely in the browser — no upload, no server touching the file. That part is non-negotiable for me; if you're going to look at your own photos you shouldn't have to POST them to a stranger first. Which means the decoder has to be WASM.&lt;/p&gt;

&lt;p&gt;Here's what that actually took, including the part where it broke in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting with jSquash
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;@jsquash/jxl&lt;/code&gt; is the obvious starting point. It's a nicely packaged WASM build of libjxl and it works. I shipped with it.&lt;/p&gt;

&lt;p&gt;Then someone on Reddit tried a 22.4 megapixel file and got:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RuntimeError: index out of bounds
    at toWireType
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not a great error message. The encoder was dying somewhere between 12 and 16 megapixels — under that, fine; over it, dead. The heap can grow to 2GB but it never gets close before falling over, so it's not a simple "raise the limit" fix.&lt;/p&gt;

&lt;p&gt;And even below the limit it was slow. A 12MP image took 21 seconds at effort 7. Nobody waits 21 seconds.&lt;/p&gt;

&lt;p&gt;So: build libjxl myself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The build
&lt;/h2&gt;

&lt;p&gt;Standard emscripten setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 https://github.com/emscripten-core/emsdk.git
&lt;span class="nb"&gt;cd &lt;/span&gt;emsdk &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./emsdk &lt;span class="nb"&gt;install &lt;/span&gt;latest &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./emsdk activate latest
git clone &lt;span class="nt"&gt;--depth&lt;/span&gt; 1 &lt;span class="nt"&gt;--recurse-submodules&lt;/span&gt; &lt;span class="nt"&gt;--shallow-submodules&lt;/span&gt; https://github.com/libjxl/libjxl.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cmake invocation is where the interesting decisions live:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/emsdk/emsdk_env.sh &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; ~/libjxl
emcmake cmake &lt;span class="nt"&gt;-B&lt;/span&gt; build-wasm &lt;span class="nt"&gt;-S&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;-DCMAKE_BUILD_TYPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;Release &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-DBUILD_SHARED_LIBS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OFF &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-DJPEGXL_ENABLE_WASM_THREADS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OFF &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-DBUILD_TESTING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OFF &lt;span class="nt"&gt;-DJPEGXL_ENABLE_BENCHMARK&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OFF &lt;span class="nt"&gt;-DJPEGXL_ENABLE_EXAMPLES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OFF &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-DJPEGXL_ENABLE_MANPAGES&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OFF &lt;span class="nt"&gt;-DJPEGXL_ENABLE_JNI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OFF &lt;span class="nt"&gt;-DJPEGXL_ENABLE_PLUGINS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OFF &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-DJPEGXL_ENABLE_VIEWERS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OFF &lt;span class="nt"&gt;-DJPEGXL_ENABLE_DOXYGEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OFF &lt;span class="nt"&gt;-DJPEGXL_ENABLE_TOOLS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ON &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"-DCMAKE_EXE_LINKER_FLAGS=-sEXPORTED_RUNTIME_METHODS=FS,callMain -sFORCE_FILESYSTEM=1 -sALLOW_MEMORY_GROWTH=1 -sINVOKE_RUN=0 -sEXIT_RUNTIME=0 -sMODULARIZE=1 -sEXPORT_NAME=createCjxl"&lt;/span&gt;

emmake make &lt;span class="nt"&gt;-C&lt;/span&gt; build-wasm cjxl &lt;span class="nt"&gt;-j6&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two of those flags are load-bearing and the rest is just trimming fat.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-DBUILD_SHARED_LIBS=OFF&lt;/code&gt; — without it the link fails looking for &lt;code&gt;libjxl_cms.so&lt;/code&gt;. Shared libraries in a WASM build don't mean what you want them to mean.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;-DJPEGXL_ENABLE_WASM_THREADS=OFF&lt;/code&gt; — this one is a real architectural choice, not a workaround. Turning threads on pulls in pthreads, which needs &lt;code&gt;SharedArrayBuffer&lt;/code&gt;, which needs you to serve the page with &lt;code&gt;Cross-Origin-Opener-Policy&lt;/code&gt; and &lt;code&gt;Cross-Origin-Embedder-Policy&lt;/code&gt; headers. Those headers put your document in a different agent cluster and quietly break most third-party embeds. If your page is nothing but the tool, go for it. If it has anything else on it, you're choosing between threads and the rest of your page working.&lt;/p&gt;

&lt;p&gt;I chose single-threaded. Which leads directly to the next thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The effort cliff
&lt;/h2&gt;

&lt;p&gt;libjxl's &lt;code&gt;-e&lt;/code&gt; flag goes from 1 to 9. Higher effort, smaller file, more CPU. Conventional wisdom says 7 is a good default.&lt;/p&gt;

&lt;p&gt;Single-threaded in WASM, on a 22.4MP image:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;effort&lt;/th&gt;
&lt;th&gt;time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;~3s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;~25s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;~49s&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's not a curve, that's a wall between 4 and 5. Eight times slower for a file size difference most people would not notice. I have not dug into which specific passes turn on at effort 5, and I'd genuinely like to know, but empirically &lt;code&gt;-e 4&lt;/code&gt; is the only setting that makes browser-side encoding of large images feel like software rather than a punishment.&lt;/p&gt;

&lt;p&gt;For comparison, small images through this path come out roughly 10x faster than jSquash at effort 7. The fix for the crash turned out to also be the fix for the speed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Feeding it pixels
&lt;/h2&gt;

&lt;p&gt;Here's a trap I walked straight into. I stripped the build down, which means there is no JPEG decoder inside it. So this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cjxl &lt;span class="k"&gt;in&lt;/span&gt;.jpg out.jxl &lt;span class="nt"&gt;--lossless_jpeg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;fails with &lt;code&gt;Getting pixel data failed&lt;/code&gt;. It can't read the JPEG.&lt;/p&gt;

&lt;p&gt;The fix is to let the browser do what browsers are already extremely good at — decode the image to a canvas — and hand cjxl raw pixels as a netpbm file. PPM for RGB, PAM when there's an alpha channel:&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="c1"&gt;// no alpha&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`P6\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n255\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;   &lt;span class="c1"&gt;// + RGB bytes&lt;/span&gt;

&lt;span class="c1"&gt;// with alpha&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;header&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`P7\nWIDTH &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nHEIGHT &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;h&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\nDEPTH 4\nMAXVAL 255\nTUPLTYPE RGB_ALPHA\nENDHDR\n`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// + RGBA bytes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&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;await&lt;/span&gt; &lt;span class="nf"&gt;loadScript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/vendor/jxl-enc/cjxl.js&lt;/span&gt;&lt;span class="dl"&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;m&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;createCjxl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;noInitialRun&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="na"&gt;locateFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/assets/vendor/jxl-enc/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;in.ppm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ppmBytes&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callMain&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;in.ppm&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;out.jxl&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;--num_threads&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;0&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;-q&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;90&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;-e&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;4&lt;/span&gt;&lt;span class="dl"&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;jxl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;out.jxl&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--num_threads 0&lt;/code&gt; is not optional. A build made without thread support aborts if you don't pass it explicitly.&lt;/p&gt;

&lt;p&gt;Also: instantiate a fresh module per file. Once an emscripten module aborts, it stays dead — the cached instance will keep failing until you reload the page, and you will spend an hour convinced your input is malformed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The path that was fine all along
&lt;/h2&gt;

&lt;p&gt;None of the above applies to lossless JPEG transcoding, which is the genuinely great trick JPEG XL has and which almost nobody knows about.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;JxlEncoderAddJPEGFrame&lt;/code&gt; doesn't decode your JPEG and re-encode it. It repacks the existing coefficients with better entropy coding. You get a file 10-20% smaller, and decoding it reconstructs the original JPEG &lt;strong&gt;bit for bit&lt;/strong&gt;. Not "visually identical" — the same bytes.&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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;callMain&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;in.jpg&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;out.jxl&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;--num_threads&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;0&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;--lossless_jpeg=1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I verified it the boring way: transcode with cjxl, reconstruct with djxl, compare sha256 of original and reconstruction. Identical.&lt;/p&gt;

&lt;p&gt;That path never had the memory problem, because it never touches pixels. 22.4MP goes through in about 0.6 seconds.&lt;/p&gt;

&lt;p&gt;If you have a photo archive sitting in JPEG, this is close to free space. Nothing is lost and it's reversible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it ended up
&lt;/h2&gt;

&lt;p&gt;Everything runs client-side. Files never leave the browser, which for a tool people point at their own photos seemed like the only defensible design.&lt;/p&gt;

&lt;p&gt;The viewer is here if it's useful to you: &lt;strong&gt;&lt;a href="https://jpegxlconvert.com/en/jxl-viewer/" rel="noopener noreferrer"&gt;https://jpegxlconvert.com/en/jxl-viewer/&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Drop a &lt;code&gt;.jxl&lt;/code&gt; in, it renders. It'll also convert to PNG or JPG if you need something the rest of your OS understands.&lt;/p&gt;

&lt;p&gt;If you know why effort 5 falls off that cliff, I'd like to hear it.&lt;/p&gt;

</description>
      <category>webassembly</category>
      <category>javascript</category>
      <category>webdev</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built a private, 100% in-browser JPEG XL converter (no upload, no server)</title>
      <dc:creator>El_Necora</dc:creator>
      <pubDate>Thu, 25 Jun 2026 10:22:14 +0000</pubDate>
      <link>https://dev.to/robertcasschdot/i-built-a-private-100-in-browser-jpeg-xl-converter-no-upload-no-server-493i</link>
      <guid>https://dev.to/robertcasschdot/i-built-a-private-100-in-browser-jpeg-xl-converter-no-upload-no-server-493i</guid>
      <description>&lt;p&gt;If you've ever downloaded a &lt;code&gt;.jxl&lt;/code&gt; file and watched your browser shrug at it, you've met the JPEG XL problem: it's a genuinely great image format that &lt;strong&gt;almost nothing opens yet&lt;/strong&gt;. So I built a small tool that converts images &lt;strong&gt;to and from JPEG XL entirely in the browser&lt;/strong&gt; — no upload, no account, no server touching your files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://jpegxlconvert.com" rel="noopener noreferrer"&gt;https://jpegxlconvert.com&lt;/a&gt; · &lt;strong&gt;Source:&lt;/strong&gt; &lt;a href="https://github.com/robertcassch-dot/jpegxlconvert" rel="noopener noreferrer"&gt;https://github.com/robertcassch-dot/jpegxlconvert&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother?
&lt;/h2&gt;

&lt;p&gt;JPEG XL (&lt;code&gt;.jxl&lt;/code&gt;) gives you smaller files at the same quality, lossless JPEG repacking (~20% off your existing JPEGs with zero quality loss), transparency, and a royalty-free spec. The catch in 2026 is &lt;strong&gt;support&lt;/strong&gt;: most browsers and apps still can't display a &lt;code&gt;.jxl&lt;/code&gt;. Chrome removed it in 2022 and is in the process of bringing it back — but until that's everywhere, people keep ending up with &lt;code&gt;.jxl&lt;/code&gt; files they can't view.&lt;/p&gt;

&lt;p&gt;Most "online converters" answer that by &lt;strong&gt;uploading your image to their server&lt;/strong&gt;. For a personal photo, that's a lot of trust for a format conversion. I wanted the opposite: the file never leaves your device.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Everything runs client-side with WebAssembly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JPG / PNG / WebP / GIF → JXL&lt;/strong&gt;: the browser decodes the source natively, then &lt;a href="https://github.com/jamsinclair/jSquash" rel="noopener noreferrer"&gt;&lt;code&gt;@jsquash/jxl&lt;/code&gt;&lt;/a&gt; (libjxl compiled to WASM) encodes the JXL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;.jxl&lt;/code&gt; → PNG / JPG&lt;/strong&gt;: decoded with &lt;code&gt;@jsquash/jxl&lt;/code&gt;, re-encoded natively via a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HEIC&lt;/strong&gt; (iPhone photos) → JPG/PNG/JXL: decoded in-browser too, so you can finally open HEIC outside Apple's world.
&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;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;encode&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;@jsquash/jxl&lt;/span&gt;&lt;span class="dl"&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;bitmap&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;createImageBitmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;          &lt;span class="c1"&gt;// native decode&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;canvas&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;OffscreenCanvas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bitmap&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;bitmap&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;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&lt;/span&gt;&lt;span class="dl"&gt;'&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;bitmap&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="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;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2d&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="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;bitmap&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;bitmap&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;jxl&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;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageData&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;90&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;  &lt;span class="c1"&gt;// WASM encode, on-device&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;fetch()&lt;/code&gt; to a backend anywhere in that path — you can verify it in the Network tab, which is kind of the whole point.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few things I learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-host the WASM codec.&lt;/strong&gt; Pulling it from a CDN at runtime works in dev, but for speed, reliability and a cleaner privacy story you really want the &lt;code&gt;.wasm&lt;/code&gt; served from your own origin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orientation metadata bites you on HEIC.&lt;/strong&gt; iPhone photos lean on EXIF orientation; if you ignore it, half your conversions come out sideways.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A "viewer" is half the product.&lt;/strong&gt; Plenty of people don't want to convert — they just want to &lt;em&gt;see&lt;/em&gt; the &lt;code&gt;.jxl&lt;/code&gt;. Decoding it to a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; and offering a one-click "save as JPG/PNG" turned out to be the most-used path.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  It's open source
&lt;/h2&gt;

&lt;p&gt;The whole static site is on GitHub: &lt;a href="https://github.com/robertcassch-dot/jpegxlconvert" rel="noopener noreferrer"&gt;https://github.com/robertcassch-dot/jpegxlconvert&lt;/a&gt;. It's multilingual (EN/ES/DE/FR/RU), deploys to any static host (I use Cloudflare Pages), and has zero backend.&lt;/p&gt;

&lt;p&gt;If you've got &lt;code&gt;.jxl&lt;/code&gt; files gathering dust, try it: &lt;a href="https://jpegxlconvert.com" rel="noopener noreferrer"&gt;https://jpegxlconvert.com&lt;/a&gt;. Feedback and issues welcome.&lt;/p&gt;

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