<?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: Ken Lee</title>
    <description>The latest articles on DEV Community by Ken Lee (@ken_lee_fc82a8ce3a147aea6).</description>
    <link>https://dev.to/ken_lee_fc82a8ce3a147aea6</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%2F3713675%2F60a8ee1b-469b-4922-aff6-f448097d8753.png</url>
      <title>DEV Community: Ken Lee</title>
      <link>https://dev.to/ken_lee_fc82a8ce3a147aea6</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ken_lee_fc82a8ce3a147aea6"/>
    <language>en</language>
    <item>
      <title>Building image &amp; video conversion that never leaves the browser</title>
      <dc:creator>Ken Lee</dc:creator>
      <pubDate>Wed, 19 Aug 2026 02:52:56 +0000</pubDate>
      <link>https://dev.to/ken_lee_fc82a8ce3a147aea6/building-image-video-conversion-that-never-leaves-the-browser-iga</link>
      <guid>https://dev.to/ken_lee_fc82a8ce3a147aea6/building-image-video-conversion-that-never-leaves-the-browser-iga</guid>
      <description>&lt;p&gt;&lt;em&gt;How VisualRefiner converts HEIC, compresses images, and transcodes video with no server, no upload, and no account — using the Canvas API, WebCodecs, and a little WebAssembly.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most "free online converters" work like this: you pick a file, it's uploaded to a server, converted there, and sent back. That's fine for a meme. It's less fine for a folder of personal photos, a passport scan, or client footage under NDA.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://visualrefiner.com" rel="noopener noreferrer"&gt;VisualRefiner&lt;/a&gt; to do the same jobs without the upload. Every conversion runs in the browser tab on your own machine. There is no backend to send files to — the whole site is a static export — so "your files never leave your device" isn't a privacy promise, it's just how the architecture works. Here's the interesting part of how it's put together.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core: &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; is a format converter
&lt;/h2&gt;

&lt;p&gt;The humble 2D canvas is already an image transcoder. Decode any image the browser understands into an &lt;code&gt;ImageBitmap&lt;/code&gt;, draw it, and re-encode with &lt;code&gt;toBlob()&lt;/code&gt; — the MIME type you pass decides the output format:&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;convert&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="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.82&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;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="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="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;canvas&lt;/span&gt;&lt;span class="dl"&gt;"&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="o"&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;canvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;height&lt;/span&gt; &lt;span class="o"&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="s2"&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="na"&gt;alpha&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="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="nx"&gt;bitmap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;toBlob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;encode failed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))),&lt;/span&gt;
      &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;// "image/jpeg" | "image/png" | "image/webp"&lt;/span&gt;
      &lt;span class="nx"&gt;quality&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// ignored for PNG (lossless)&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 single function is PNG↔JPG↔WebP conversion and JPG/WebP compression (the &lt;code&gt;quality&lt;/code&gt; argument) all at once. No dependency, no upload, works offline. The result is a &lt;code&gt;Blob&lt;/code&gt;; wrap it in &lt;code&gt;URL.createObjectURL()&lt;/code&gt; for a preview or a download link.&lt;/p&gt;

&lt;p&gt;Two details that bite you if you skip them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Revoke your object URLs.&lt;/strong&gt; Each &lt;code&gt;createObjectURL&lt;/code&gt; pins the blob in memory until you &lt;code&gt;URL.revokeObjectURL()&lt;/code&gt; it. In a tool people use repeatedly, forgetting this is a slow leak. Revoke the previous URL every time you produce a new one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PNG ignores &lt;code&gt;quality&lt;/code&gt;.&lt;/strong&gt; A "quality slider" on a PNG export does nothing — PNG is lossless. Hide the control or convert to WebP if the user wants a smaller file with a quality knob.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  HEIC: the format the canvas can't read
&lt;/h2&gt;

&lt;p&gt;Then there's HEIC — the format iPhones save by default. Most browsers &lt;em&gt;won't&lt;/em&gt; decode it in a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; pipeline, so &lt;code&gt;createImageBitmap()&lt;/code&gt; throws. The fix is to decode it ourselves first, with &lt;a href="https://github.com/strukturag/libheif" rel="noopener noreferrer"&gt;libheif&lt;/a&gt; compiled to WebAssembly (via &lt;a href="https://github.com/alexcorvi/heic2any" rel="noopener noreferrer"&gt;heic2any&lt;/a&gt;):&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;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;normalize&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="nx"&gt;quality&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;isHeic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/heic|heif/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;hei&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;cf&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;$/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;isHeic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&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;// Only pulled in when someone actually hands us a HEIC file.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;heic2any&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;heic2any&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;out&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;heic2any&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;toType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image/jpeg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;quality&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;out&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="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;out&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;Now HEIC flows into the same canvas function as everything else. The WASM decoder is the heaviest thing on the site (~2 MB), which brings up the next point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Load the heavy parts lazily
&lt;/h2&gt;

&lt;p&gt;If you bundle libheif, a video engine, and a GIF encoder into your main chunk, your homepage pays for tools the visitor may never touch. The trick is a dynamic &lt;code&gt;import()&lt;/code&gt; at the moment of use — note the &lt;code&gt;await import(...)&lt;/code&gt; inside both functions above and below. The bundler splits each into its own chunk, and the network tab confirms the 2 MB HEIC decoder only downloads when someone converts a HEIC file. The &lt;a href="https://visualrefiner.com" rel="noopener noreferrer"&gt;homepage&lt;/a&gt; ships a small bundle; the weight arrives on demand.&lt;/p&gt;

&lt;p&gt;High-quality &lt;strong&gt;resizing&lt;/strong&gt; is the same story. Canvas &lt;code&gt;drawImage&lt;/code&gt; downscaling aliases badly (jagged edges, moiré). &lt;a href="https://github.com/nodeca/pica" rel="noopener noreferrer"&gt;pica&lt;/a&gt; does proper Lanczos-style resampling, so it's loaded only on the resize path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;needsResize&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="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;pica&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pica&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pica&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;resize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sourceCanvas&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;targetCanvas&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;h2&gt;
  
  
  Video, without FFmpeg-in-WASM
&lt;/h2&gt;

&lt;p&gt;Video is where people reach for &lt;code&gt;ffmpeg.wasm&lt;/code&gt;, but it's a big download and slow. Modern browsers expose &lt;a href="https://developer.mozilla.org/docs/Web/API/WebCodecs_API" rel="noopener noreferrer"&gt;WebCodecs&lt;/a&gt; — hardware-accelerated encode/decode built into the browser. VisualRefiner drives it through &lt;a href="https://github.com/Vanilagy/mediabunny" rel="noopener noreferrer"&gt;mediabunny&lt;/a&gt; to transcode clips to MP4 or WebM, extract frames as PNG, or build a GIF (with &lt;a href="https://github.com/mattdesl/gifenc" rel="noopener noreferrer"&gt;gifenc&lt;/a&gt;) — all in the tab.&lt;/p&gt;

&lt;p&gt;The honest caveat: WebCodecs support and the available codecs &lt;strong&gt;depend on the browser&lt;/strong&gt;. So conversion isn't guaranteed for every exotic input the way the canvas image path is. The tool checks and tells you when a track can't be decoded, rather than pretending. That trade — depend on the platform, degrade honestly — beats shipping a 25 MB WASM FFmpeg to every visitor.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why static export makes the privacy claim trivially true
&lt;/h2&gt;

&lt;p&gt;The site is a Next.js app exported to static files (&lt;code&gt;output: "export"&lt;/code&gt;) and served as plain assets from a CDN. There is no API route, no server that receives a file, nothing to log. You don't have to trust a privacy policy — open DevTools → Network, convert something, and watch: &lt;strong&gt;zero upload requests&lt;/strong&gt;. The bytes stay in the tab.&lt;/p&gt;

&lt;p&gt;That also means the "server cost" of a converter that could handle thousands of files is… a static host. All the compute is the user's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; + &lt;code&gt;toBlob()&lt;/code&gt; is a complete image converter/compressor with zero deps.&lt;/li&gt;
&lt;li&gt;For formats the browser can't decode (HEIC), a WASM decoder bridges the gap — load it lazily so everyone else doesn't pay for it.&lt;/li&gt;
&lt;li&gt;Prefer WebCodecs over WASM FFmpeg for video when you can accept browser-dependent codec support.&lt;/li&gt;
&lt;li&gt;Dynamic &lt;code&gt;import()&lt;/code&gt; per feature keeps the initial bundle small.&lt;/li&gt;
&lt;li&gt;A static export turns "we don't upload your files" from a promise into an architectural fact.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to see it in action, the tools are at &lt;a href="https://visualrefiner.com" rel="noopener noreferrer"&gt;visualrefiner.com&lt;/a&gt; — &lt;a href="https://visualrefiner.com/heic-to-jpg" rel="noopener noreferrer"&gt;HEIC → JPG&lt;/a&gt;, &lt;a href="https://visualrefiner.com/image-compressor" rel="noopener noreferrer"&gt;image compressor&lt;/a&gt;, &lt;a href="https://visualrefiner.com/video-to-gif" rel="noopener noreferrer"&gt;video to GIF&lt;/a&gt; — and the code is on &lt;a href="https://github.com/ken-fs/VisualRefiner" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

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