<?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: river</title>
    <description>The latest articles on DEV Community by river (@river_jiang_670cccce7d97d).</description>
    <link>https://dev.to/river_jiang_670cccce7d97d</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%2F3856668%2F2b71e26f-87c6-480c-a7c8-dba75b81baa1.png</url>
      <title>DEV Community: river</title>
      <link>https://dev.to/river_jiang_670cccce7d97d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/river_jiang_670cccce7d97d"/>
    <language>en</language>
    <item>
      <title>Convert a PNG Sequence to Transparent WebM with FFmpeg</title>
      <dc:creator>river</dc:creator>
      <pubDate>Sat, 31 Oct 2026 17:31:41 +0000</pubDate>
      <link>https://dev.to/river_jiang_670cccce7d97d/convert-a-png-sequence-to-transparent-webm-with-ffmpeg-3ggd</link>
      <guid>https://dev.to/river_jiang_670cccce7d97d/convert-a-png-sequence-to-transparent-webm-with-ffmpeg-3ggd</guid>
      <description>&lt;p&gt;The PNG frames are transparent. The exported video has a black rectangle. Running background removal again seems like the obvious fix, but it can be the wrong step: the alpha channel may have disappeared during encoding, or the player may simply be ignoring it.&lt;/p&gt;

&lt;p&gt;To convert an existing transparent PNG sequence to WebM, use FFmpeg with &lt;code&gt;libvpx-vp9&lt;/code&gt; and an alpha-capable pixel format. Then decode the output and check it. Renaming a file to &lt;code&gt;.webm&lt;/code&gt; does not create transparency, and a successful encode does not prove that transparency survived.&lt;/p&gt;

&lt;p&gt;This walkthrough uses real footage, an opaque control, and a frame-by-frame comparison. There is also a small result worth keeping in mind: in this particular run, the largest alpha difference was &lt;strong&gt;one level out of 255&lt;/strong&gt;, even with the encoder's lossless option enabled.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr18ol86ccfchg6sr707w.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fr18ol86ccfchg6sr707w.png" alt="A real video frame before background removal, its converted transparent output on two backgrounds, and the decoded alpha plane." width="800" height="611"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The source cutout is the public podcast demo from &lt;a href="https://bgremover.online/" rel="noopener noreferrer"&gt;BG Remover Online&lt;/a&gt;. Original footage: &lt;a href="https://mixkit.co/free-stock-video/bearded-announcer-close-up-2960/" rel="noopener noreferrer"&gt;Mixkit, “Bearded announcer, close up”&lt;/a&gt;. The person shown is not affiliated. The processed cutout also removes the microphone; it is not a perfect subject-selection example. The backgrounds in the figure are display layers, not baked into the PNG.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with frames that already contain transparency
&lt;/h2&gt;

&lt;p&gt;A PNG can contain alpha, but it can also be completely opaque. Open a frame above two different solid backgrounds. The empty area around the subject should reveal each background. A checkerboard that stays fixed inside the image may be ordinary image pixels.&lt;/p&gt;

&lt;p&gt;Keep the frames in a dedicated directory with matching dimensions and continuous numbering:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;frames/
  frame_0001.png
  frame_0002.png
  frame_0003.png
  ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command below expects four digits and a first frame numbered &lt;code&gt;0001&lt;/code&gt;. Use &lt;code&gt;%03d&lt;/code&gt; for three digits, or change &lt;code&gt;-start_number&lt;/code&gt; if your sequence begins elsewhere. A missing number can make a sequence end early. Check how many frames you actually encoded.&lt;/p&gt;

&lt;p&gt;For the same conversion in a browser, the &lt;a href="https://bgremover.online/converters/png-sequence-to-webm/" rel="noopener noreferrer"&gt;PNG sequence to transparent WebM converter&lt;/a&gt; accepts a ZIP of PNG frames and outputs VP9-alpha WebM. Google sign-in is required; check the current file limits before uploading. This is an alternative workflow, not the workflow benchmarked below.&lt;/p&gt;

&lt;p&gt;This step packages existing cutouts into video. If your starting point is an ordinary MP4 with a room behind the presenter, you still need a separate matting, masking, or keying step. A pixel-format flag cannot identify which parts of that room to remove.&lt;/p&gt;

&lt;h2&gt;
  
  
  Encode the sequence
&lt;/h2&gt;

&lt;p&gt;Use an FFmpeg build with the &lt;code&gt;libvpx-vp9&lt;/code&gt; encoder. In a terminal, check its availability:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-h&lt;/span&gt; &lt;span class="nv"&gt;encoder&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;libvpx-vp9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then run this from the directory containing &lt;code&gt;frames&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-framerate&lt;/span&gt; 12 &lt;span class="nt"&gt;-start_number&lt;/span&gt; 1 &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"frames/frame_%04d.png"&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt;:v libvpx-vp9 &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; yuva420p &lt;span class="nt"&gt;-lossless&lt;/span&gt; 1 &lt;span class="nt"&gt;-auto-alt-ref&lt;/span&gt; 0 cutout.webm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the encoding command used in the experiment. If &lt;code&gt;cutout.webm&lt;/code&gt; already exists, FFmpeg asks before overwriting it. Use a new output name when comparing settings.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Setting&lt;/th&gt;
&lt;th&gt;What it controls&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-framerate 12&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reads the sequence at 12 frames per second. Set it to the rate your frames were created for.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-start_number 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Starts at &lt;code&gt;frame_0001.png&lt;/code&gt;.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-c:v libvpx-vp9&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Selects the VP9 encoder used here.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-pix_fmt yuva420p&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Requests an alpha-capable pixel format. The &lt;code&gt;a&lt;/code&gt; matters.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-lossless 1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Enables the encoder's lossless mode. It does not promise that every conversion around the encoder is numerically lossless.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-auto-alt-ref 0&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Disables alternate reference frames; this is the setting used for the tested alpha workflow.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Input frame rate belongs before &lt;code&gt;-i&lt;/code&gt;. In this example, 24 frames at 12 fps produce two seconds of video. Reading the same files at 24 fps produces one second. Neither command reconstructs the original timing of an irregularly timed sequence.&lt;/p&gt;

&lt;p&gt;FFmpeg documents the numbering and frame-rate behavior in its &lt;a href="https://www.ffmpeg.org/ffmpeg-formats.html#image2-1" rel="noopener noreferrer"&gt;image2 input documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The result has no audio because the input contains only images. If you need sound, preserve it separately and align it against the final duration. Do not assume changing the frame rate will keep an existing voice track in sync.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the real-footage test showed
&lt;/h2&gt;

&lt;p&gt;The test ran on Windows with FFmpeg 8.0.1. Its input was an already processed &lt;a href="https://bgremover.online/demo/podcast-after-alpha.webm" rel="noopener noreferrer"&gt;BG Remover Online demo cutout&lt;/a&gt;, not a newly submitted background-removal job.&lt;/p&gt;

&lt;p&gt;The setup extracted the first two seconds at 12 fps, resized to 640 × 360, and wrote 24 RGBA PNGs. That extraction explicitly used the libvpx VP9 decoder so the existing WebM alpha could be retained:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-c&lt;/span&gt;:v libvpx-vp9 &lt;span class="nt"&gt;-i&lt;/span&gt; podcast-after-alpha.webm &lt;span class="nt"&gt;-t&lt;/span&gt; 2 &lt;span class="nt"&gt;-vf&lt;/span&gt; &lt;span class="s2"&gt;"fps=12,scale=640:360"&lt;/span&gt; &lt;span class="nt"&gt;-pix_fmt&lt;/span&gt; rgba &lt;span class="nt"&gt;-start_number&lt;/span&gt; 1 &lt;span class="s2"&gt;"frames/frame_%04d.png"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create the &lt;code&gt;frames&lt;/code&gt; directory first. You can skip this extraction entirely when you already have your own PNG sequence. For a browser-based reverse workflow, the &lt;a href="https://bgremover.online/converters/video-to-png-sequence/" rel="noopener noreferrer"&gt;video to PNG sequence converter&lt;/a&gt; returns RGBA PNG frames in a ZIP.&lt;/p&gt;

&lt;p&gt;After encoding, every output frame was decoded to RGBA and its alpha bytes were compared with the corresponding PNG. The result:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Observation&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PNG frames read / WebM frames decoded&lt;/td&gt;
&lt;td&gt;24 / 24&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output dimensions&lt;/td&gt;
&lt;td&gt;640 × 360&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output frame rate / duration&lt;/td&gt;
&lt;td&gt;12 fps / 2.000 seconds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output file size&lt;/td&gt;
&lt;td&gt;1,056,049 bytes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Alpha range in every decoded frame&lt;/td&gt;
&lt;td&gt;0–255&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Largest absolute alpha difference across all 24 frames&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bit-identical alpha across every frame&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row is easy to miss. On the first frame, 89,034 alpha values differed, but no value differed by more than one. The count of completely transparent pixels remained 137,838. Many near-opaque values became fully opaque.&lt;/p&gt;

&lt;p&gt;This does not establish which internal step caused the difference, or predict another FFmpeg build's behavior. It establishes what this complete workflow produced. If your acceptance criterion is “a visible cutout with transparent surroundings,” these observations are relevant. If it is “preserve every alpha byte exactly,” this run fails that stricter requirement.&lt;/p&gt;

&lt;p&gt;RGB equality was not tested or claimed. The requested output uses YUV 4:2:0, so preserving alpha is a separate question from retaining all source color detail.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmbjvjx6p6sqw1k8r39nz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fmbjvjx6p6sqw1k8r39nz.png" alt="Measured results for the 24-frame conversion: transparency retained, maximum alpha difference of one, and an opaque control with alpha 255." width="800" height="434"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Decode the output before trusting the metadata
&lt;/h2&gt;

&lt;p&gt;First, extract a PNG with the libvpx decoder selected &lt;strong&gt;before&lt;/strong&gt; the input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-c&lt;/span&gt;:v libvpx-vp9 &lt;span class="nt"&gt;-i&lt;/span&gt; cutout.webm &lt;span class="nt"&gt;-frames&lt;/span&gt;:v 1 check.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;check.png&lt;/code&gt; in an image editor over light and dark backgrounds. For a clearer look at opacity itself, extract the alpha plane:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ffmpeg &lt;span class="nt"&gt;-c&lt;/span&gt;:v libvpx-vp9 &lt;span class="nt"&gt;-i&lt;/span&gt; cutout.webm &lt;span class="nt"&gt;-vf&lt;/span&gt; alphaextract &lt;span class="nt"&gt;-frames&lt;/span&gt;:v 1 alpha.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;alpha.png&lt;/code&gt;, black means transparent, white means opaque, and gray means partially opaque. It is a grayscale diagnostic image, not a second cutout. See FFmpeg's &lt;a href="https://www.ffmpeg.org/ffmpeg-filters.html#alphaextract" rel="noopener noreferrer"&gt;alphaextract documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this run, ffprobe reported &lt;code&gt;pix_fmt=yuv420p&lt;/code&gt; alongside &lt;code&gt;alpha_mode=1&lt;/code&gt; for a file whose decoded frames contained transparency. Therefore, that pixel-format field alone was not a reliable rejection rule. Conversely, alpha metadata alone does not establish that any pixel is actually transparent.&lt;/p&gt;

&lt;p&gt;The control made that distinction visible. The same sequence was encoded with &lt;code&gt;yuv420p&lt;/code&gt;, without the &lt;code&gt;a&lt;/code&gt;. Its first decoded frame had alpha 255 everywhere: an opaque rectangle. That is the sort of output a conversion pipeline can create while still returning a playable video.&lt;/p&gt;

&lt;p&gt;Checking the first frame is a quick diagnostic, not a whole-clip guarantee. The 24-frame comparison above inspected this entire short fixture; a longer production export needs its own inspection scope.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put it over the actual page background
&lt;/h2&gt;

&lt;p&gt;A file-level check does not answer whether the destination browser composites it correctly. Save this next to &lt;code&gt;cutout.webm&lt;/code&gt; as &lt;code&gt;preview.html&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;!doctype html&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;html&lt;/span&gt; &lt;span class="na"&gt;lang=&lt;/span&gt;&lt;span class="s"&gt;"en"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;charset=&lt;/span&gt;&lt;span class="s"&gt;"utf-8"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"viewport"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"width=device-width, initial-scale=1"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Transparent WebM check&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.stage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;max-width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;640px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;#17263d&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.stage.light&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;#f5ead6&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;video&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"background"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Switch background&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"stage"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"stage"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;video&lt;/span&gt; &lt;span class="na"&gt;controls&lt;/span&gt; &lt;span class="na"&gt;muted&lt;/span&gt; &lt;span class="na"&gt;loop&lt;/span&gt; &lt;span class="na"&gt;playsinline&lt;/span&gt; &lt;span class="na"&gt;preload=&lt;/span&gt;&lt;span class="s"&gt;"metadata"&lt;/span&gt;
    &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"cutout.webm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/video&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;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;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#background&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&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;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#stage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/html&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Play it, switch backgrounds, and look at hair, fingers, and motion-blurred edges. A flat rectangle suggests opaque pixels or an unsupported playback path. A changing background with halos suggests a different problem: the matte or color edges need work.&lt;/p&gt;

&lt;p&gt;Do not use CSS &lt;code&gt;mix-blend-mode: screen&lt;/code&gt; as proof that transparency works. It can hide dark pixels while also changing the subject. A real alpha channel should composite without that workaround.&lt;/p&gt;

&lt;p&gt;Browser support needs its own check. Chrome documents &lt;a href="https://developer.chrome.com/blog/alpha-transparency-in-chrome-video" rel="noopener noreferrer"&gt;WebM alpha playback&lt;/a&gt;. MDN's &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Video_codecs#vp9" rel="noopener noreferrer"&gt;codec guide&lt;/a&gt; notes that Safari does not support VP9 alpha. This experiment did not test Safari. Keep a tested fallback for browsers outside your validated path; a static transparent PNG may be enough for a decorative animation.&lt;/p&gt;

&lt;p&gt;For an editing handoff rather than a web page, the &lt;a href="https://bgremover.online/converters/webm-to-prores/" rel="noopener noreferrer"&gt;transparent WebM to ProRes 4444 converter&lt;/a&gt; provides an alpha-capable MOV workflow for editing software. ProRes is an editing format, not a substitute for a tested browser delivery format. These online converters require sign-in and have usage limits; they were not part of the local FFmpeg benchmark above.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the result still looks wrong
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Check next&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;“Unknown encoder libvpx-vp9”&lt;/td&gt;
&lt;td&gt;Use a build that includes the encoder; the filename extension cannot substitute for it.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Video stops earlier than expected&lt;/td&gt;
&lt;td&gt;Check continuous numbering, the first-frame index, the encoded frame count, and input frame rate.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Solid background in the extracted PNG&lt;/td&gt;
&lt;td&gt;Confirm the source PNG has alpha, the encoding requested &lt;code&gt;yuva420p&lt;/code&gt;, and the decoding used libvpx.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PNG is transparent, player shows black&lt;/td&gt;
&lt;td&gt;Test another known alpha-aware playback path before repeating background removal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dark or bright fringes around hair&lt;/td&gt;
&lt;td&gt;Inspect the source matte and color treatment on both backgrounds. Conversion does not repair an inaccurate cutout.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File is too large&lt;/td&gt;
&lt;td&gt;Evaluate a separate constant-quality encode against the lossless baseline, comparing edges and alpha as well as file size.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For a delivery-sized experiment, a common starting point is &lt;code&gt;-crf 30 -b:v 0&lt;/code&gt; in place of &lt;code&gt;-lossless 1&lt;/code&gt;. That variation was not benchmarked in the recorded run. Recheck its output rather than applying the lossless run's measurements to it.&lt;/p&gt;

&lt;p&gt;The useful handoff is the encoded file plus a check in its destination. Keep the PNG sequence until both have passed. It is the easiest way to change the encoding later without paying for another matting pass or discarding the best source you have.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Context and reproducibility:&lt;/strong&gt; This walkthrough accompanies the BG Remover Online project and uses its existing public demo asset. It is a conversion experiment, not an independent product comparison or a claim about all background-removal results. The recorded run was executed on November 1, 2026; its script and raw observations are retained with the draft.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI disclosure:&lt;/strong&gt; This article was drafted by an AI agent. The stated FFmpeg experiment was executed; browser compatibility beyond the documented references is not presented as a firsthand test.&lt;/p&gt;

</description>
      <category>software</category>
      <category>tools</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Test Video Transparency in CI with FFmpeg and Node.js</title>
      <dc:creator>river</dc:creator>
      <pubDate>Mon, 26 Oct 2026 08:42:17 +0000</pubDate>
      <link>https://dev.to/river_jiang_670cccce7d97d/test-video-transparency-in-ci-with-ffmpeg-and-nodejs-166d</link>
      <guid>https://dev.to/river_jiang_670cccce7d97d/test-video-transparency-in-ci-with-ffmpeg-and-nodejs-166d</guid>
      <description>&lt;p&gt;A video looks cut out in a preview. Then you place it over a dark page, and a rectangle appears around the presenter. Before an export reaches production, the useful question is whether decoded pixels actually let the new background show through.&lt;/p&gt;

&lt;p&gt;Start with a real example. The images below come from an original podcast clip and its processed transparent WebM, decoded locally. The same output frame is shown on a checkerboard, a dark background, and a light background.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd87zzoqpvk3lcklu3rf6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fd87zzoqpvk3lcklu3rf6.png" alt="Real podcast footage before removal and the actual decoded transparent output, composited on checkerboard, dark and light backgrounds." width="800" height="570"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Public &lt;a href="https://bgremover.online/" rel="noopener noreferrer"&gt;BG Remover Online demo&lt;/a&gt;, using stock footage from &lt;a href="https://mixkit.co/free-stock-video/bearded-announcer-close-up-2960/" rel="noopener noreferrer"&gt;Mixkit: “Bearded announcer, close up”&lt;/a&gt;. The person shown is not affiliated. The images are actual frames requested at 0.4 seconds; comparison backgrounds are added here. The processing also removed the foreground microphone.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;An independent run of the inspector reported &lt;code&gt;transparency-detected&lt;/code&gt; for this 1920 × 1080, 3.753-second VP9 file. At all four requested sample times—0, 0.93825, 1.8765, and 2.81475 seconds—the decoded alpha range included both 0 and 255. Each sample also contained partially transparent pixels. These observations establish sampled alpha presence, not a whole-clip matting-quality score.&lt;/p&gt;

&lt;p&gt;With the source checkout and FFmpeg below installed, reproduce that check on the public file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fL&lt;/span&gt; https://bgremover.online/demo/podcast-after-alpha.webm &lt;span class="nt"&gt;-o&lt;/span&gt; podcast-alpha.webm
node dist/cli.js podcast-alpha.webm &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="nt"&gt;--require-transparency&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real footage shows why the check matters. For a regression test, we also need inputs with known intent: a useful cutout, an opaque video, an invisible video, and a brief alpha change between sample times. The rest of this tutorial creates those controls and turns the sampled condition into a CI gate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Decide what a passing export means
&lt;/h2&gt;

&lt;p&gt;For a reusable cutout, there are at least two requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Some sampled pixels let the background show through.&lt;/li&gt;
&lt;li&gt;Some sampled pixels remain visible.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That policy rejects an entirely invisible clip. It also differs from “the format supports alpha” and “this player shows the correct result.”&lt;/p&gt;

&lt;p&gt;The experiment uses &lt;a href="https://github.com/j-river/video-alpha-inspector" rel="noopener noreferrer"&gt;video-alpha-inspector&lt;/a&gt;, a standalone Node.js wrapper around local FFmpeg decoding. It reports metadata separately from pixel observations.&lt;/p&gt;

&lt;p&gt;Its default 8-bit alpha rules are:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Rule&lt;/th&gt;
&lt;th&gt;Default&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Transparent pixel&lt;/td&gt;
&lt;td&gt;Alpha below 245&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Visible pixel&lt;/td&gt;
&lt;td&gt;Alpha above 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Significant amount&lt;/td&gt;
&lt;td&gt;At least 1% of sampled pixels, rounded up&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Requested times&lt;/td&gt;
&lt;td&gt;0%, 25%, 50%, and 75% of reported duration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The two pixel categories intentionally overlap: a partly transparent pixel can still be visible. The aggregate passing status can also combine transparency in one sampled frame with visibility in another. If a pipeline requires both in the &lt;em&gt;same&lt;/em&gt; frame, it needs a stricter per-sample policy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reproduce the run
&lt;/h2&gt;

&lt;p&gt;Use Node.js 22 or newer, plus FFmpeg and ffprobe on PATH. The FFmpeg build needs the &lt;code&gt;libvpx-vp9&lt;/code&gt; encoder and decoder for this experiment.&lt;/p&gt;

&lt;p&gt;npm publication is pending, so use the source checkout:&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 https://github.com/j-river/video-alpha-inspector.git
&lt;span class="nb"&gt;cd &lt;/span&gt;video-alpha-inspector
git checkout a20d1844535111b139ca7ec2524c5c944f897906
npm ci &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt;
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The results below were produced on Windows with Node.js 24.13.1 and FFmpeg/ffprobe 8.0.1. Different FFmpeg builds should be checked rather than assumed equivalent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Generate controls with known intent
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;make-fixtures.mjs&lt;/code&gt; in the repository root:&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Open the fixture generator: four synthetic videos
  &lt;br&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;mkdirSync&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;node:fs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;join&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;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;spawnSync&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;node:child_process&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;directory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alpha-fixtures&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;mkdirSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;recursive&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;generate&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="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&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;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;spawnSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ffmpeg&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-nostdin&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="s1"&gt;-hide_banner&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="s1"&gt;-loglevel&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="s1"&gt;error&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="s1"&gt;-f&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="s1"&gt;lavfi&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="s1"&gt;-i&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;`color=c=blue:s=64x64:r=10:d=&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;duration&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;,format=rgba`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;-vf&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;,format=yuva420p&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="s1"&gt;-c:v&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="s1"&gt;libvpx-vp9&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="s1"&gt;-lossless&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="s1"&gt;1&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="s1"&gt;-auto-alt-ref&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="s1"&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="s1"&gt;-g&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="s1"&gt;9999&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="s1"&gt;-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="s1"&gt;1&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="s1"&gt;-deadline&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="s1"&gt;realtime&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="s1"&gt;-y&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;directory&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="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;encoding&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;windowsHide&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;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&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="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cutout.webm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colorchannelmixer=aa=0,drawbox=x=16:y=16:w=32:h=32:color=blue@1:t=fill:replace=1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;tagged-opaque.webm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colorchannelmixer=aa=1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;empty.webm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;colorchannelmixer=aa=0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nf"&gt;generate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;brief-alpha.webm&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;colorchannelmixer=aa=0.5:enable='between(t,1.2,1.7)'&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Generated four synthetic VP9-alpha fixtures.&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;/p&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node make-fixtures.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The files contain synthetic colors and a square, with no downloaded footage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;cutout.webm&lt;/code&gt;: an opaque square on a transparent canvas.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tagged-opaque.webm&lt;/code&gt;: a fully opaque alpha plane.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;empty.webm&lt;/code&gt;: alpha zero everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;brief-alpha.webm&lt;/code&gt;: a four-second clip with partial alpha only around 1.2–1.7 seconds.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The generator enables the alpha encoding path for every control. The inspection below checks what actually survived encoding.&lt;/p&gt;

&lt;p&gt;These commands overwrite the named files inside &lt;code&gt;alpha-fixtures&lt;/code&gt;. Keep that folder separate from source media.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07dfwmz9q72rlsgpivr7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F07dfwmz9q72rlsgpivr7.png" alt="Actual decoded synthetic controls on light and dark backgrounds: visible cutout, opaque video, invisible video, and a partial-alpha frame." width="800" height="467"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The blue squares are deliberately simple regression controls, separate from the real footage above. The fourth column uses a focused request near 1.4 seconds.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspect pixels, then compare the outcomes
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;inspect-fixtures.mjs&lt;/code&gt; in the same repository root:&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Open the inspection script and assertions
  &lt;br&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="nx"&gt;assert&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;node:assert/strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;join&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;node:path&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;inspectVideoAlpha&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;./dist/index.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;directory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;alpha-fixtures&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;expected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cutout.webm&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="s1"&gt;transparency-detected&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="s1"&gt;tagged-opaque.webm&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="s1"&gt;no-transparency-observed&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="s1"&gt;empty.webm&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="s1"&gt;fully-transparent-samples&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="s1"&gt;brief-alpha.webm&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="s1"&gt;no-transparency-observed&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&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="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;status&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;expected&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;report&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;inspectVideoAlpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;directory&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;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;cutout.webm&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="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alpha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alpha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;empty.webm&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="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alpha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alpha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;every&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sample&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alpha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sample&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alpha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alphaMetadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alphaMetadata&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;pixelFormat&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;video&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pixelFormat&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alphaMin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;samples&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;alpha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alphaMax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;samples&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;alpha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;report&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;focused&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;inspectVideoAlpha&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;directory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;brief-alpha.webm&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;sampleFractions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.35&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;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;focused&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transparency-detected&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;focused&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;fraction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;requestedTimeSeconds&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;focused&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;samples&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;requestedTimeSeconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;focused&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alphaMin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;focused&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;samples&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;alpha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;min&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;alphaMax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;focused&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;samples&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;alpha&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;max&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;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;p&gt;Run &lt;code&gt;node inspect-fixtures.mjs&lt;/code&gt;. The assertions passed, with these observations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Alpha metadata&lt;/th&gt;
&lt;th&gt;ffprobe pixel format&lt;/th&gt;
&lt;th&gt;Default sampled alpha&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cutout.webm&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;yuv420p&lt;/td&gt;
&lt;td&gt;Values include 0 and 255&lt;/td&gt;
&lt;td&gt;transparency-detected&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tagged-opaque.webm&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;yuv420p&lt;/td&gt;
&lt;td&gt;All 255&lt;/td&gt;
&lt;td&gt;no-transparency-observed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;empty.webm&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;yuv420p&lt;/td&gt;
&lt;td&gt;All 0&lt;/td&gt;
&lt;td&gt;fully-transparent-samples&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;brief-alpha.webm&lt;/td&gt;
&lt;td&gt;true&lt;/td&gt;
&lt;td&gt;yuv420p&lt;/td&gt;
&lt;td&gt;All 255 at requested samples&lt;/td&gt;
&lt;td&gt;no-transparency-observed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Here, all four files have the same metadata hint and reported pixel format. Their decoded content leads to three different statuses.&lt;/p&gt;

&lt;p&gt;That is why a metadata-only gate cannot make the decision. This library explicitly selects the libvpx decoder for VP8/VP9 to preserve the WebM alpha data, then measures the decoded alpha plane. FFmpeg's &lt;a href="https://ffmpeg.org/ffmpeg-filters.html#alphaextract" rel="noopener noreferrer"&gt;alphaextract filter documentation&lt;/a&gt; describes the underlying extraction operation.&lt;/p&gt;

&lt;p&gt;This experiment establishes the behavior of these fixtures and this decode path. It does not establish transparency support in every browser or video player.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11krbnr8w3xc1qq71ksn.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F11krbnr8w3xc1qq71ksn.png" alt="Four-second fixture timeline showing default requests at 0, 1, 2, and 3 seconds missing the 1.2-to-1.7-second alpha interval." width="800" height="378"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The red requests miss the green interval. A focused 35% request reveals the partial alpha; requested times are not exact decoded timestamps.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The control that the default sampling misses
&lt;/h2&gt;

&lt;p&gt;The fourth fixture is four seconds long. Default sampling requests frames around 0, 1, 2, and 3 seconds. Its partial transparency occurs between two of those requests.&lt;/p&gt;

&lt;p&gt;The additional &lt;code&gt;sampleFractions: [0.35]&lt;/code&gt; call asks for a frame around 1.4 seconds. In this run, it returned:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"requestedTimeSeconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1.4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"transparency-detected"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alphaMin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;129&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"alphaMax"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;129&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The measured alpha is near half opacity. Its exact 8-bit value should not be generalized to every encoding path.&lt;/p&gt;

&lt;p&gt;More importantly, the default negative result did not mean “this clip contains no transparent frames.” It meant no qualifying transparency was observed in the requested samples.&lt;/p&gt;

&lt;p&gt;The requested seek time is not a measured frame timestamp. FFmpeg may return the next available frame. Downscaling can also remove small features from the observations.&lt;/p&gt;

&lt;p&gt;If a release requires whole-clip validation, design a bounded full-frame scan with an explicit alpha policy, or inspect all relevant frames in a trusted processing stage. Increasing a sparse sample count still does not turn sampling into a whole-clip guarantee.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6pf3li5bpb1x9c0vdqk.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6pf3li5bpb1x9c0vdqk.gif" alt="Animated sequence of decoded frames showing the brief alpha change over light and dark backgrounds." width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A slowed sequence of actual FFmpeg seek observations, not a real-time player recording. Frame requests and decoded alpha ranges are displayed.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn the observation into a CI gate
&lt;/h2&gt;

&lt;p&gt;After building, the CLI can require the library's visible-cutout condition:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node dist/cli.js alpha-fixtures/cutout.webm &lt;span class="nt"&gt;--json&lt;/span&gt; &lt;span class="nt"&gt;--require-transparency&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tested exit-code contract is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Exit code&lt;/th&gt;
&lt;th&gt;Interpretation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;cutout.webm&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;The sampled condition passed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tagged-opaque.webm&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Inspection completed; condition was unmet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;empty.webm&lt;/td&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Inspection completed; no visible cutout was established&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;missing.webm&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Inspection failed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In a CI step that already has the repository and required tools, use the command directly so its nonzero exit can fail the step. Preserve the JSON report as an artifact using the CI system's artifact mechanism.&lt;/p&gt;

&lt;p&gt;Do not append &lt;code&gt;|| true&lt;/code&gt;, swallow the exit status, or convert decoder errors into “opaque video.” A missing tool, missing sample, or damaged file is an inspection failure. The JSON report should support diagnosis, not replace the gate's exit status.&lt;/p&gt;

&lt;p&gt;This gate is appropriate only if its sampling policy is enough for the pipeline. It does not judge hair quality, temporal shimmer, color fringes, or whether a target application interprets alpha correctly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ror232sn7ejgin822ms.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F1ror232sn7ejgin822ms.png" alt="Verified CLI outcomes: exit 0 passes the sampled condition, exit 1 means unmet, and exit 2 means inspection failed." width="800" height="356"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Keeping these outcomes separate prevents a missing file or decoder failure from masquerading as a successful inspection.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep a player test in the delivery workflow
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://huggingface.co/spaces/j-river/video-alpha-inspector" rel="noopener noreferrer"&gt;browser playground&lt;/a&gt; is a separate way to inspect synthetic samples and local WebM files. It uses the browser's decoder and Canvas, so it may observe different behavior from FFmpeg. Local file contents are processed in browser memory.&lt;/p&gt;

&lt;p&gt;For a web overlay, retain a check in the destination browser. For an editing handoff, composite the actual output in the destination editor. Passing a decoded-pixel gate answers one part of delivery; those checks answer the remaining parts.&lt;/p&gt;

&lt;p&gt;The four fixtures provide a useful starting regression set: useful cutout, opaque false positive, invisible output, and an event between samples.&lt;/p&gt;

&lt;p&gt;If you already validate media exports in CI, does the gate promise a sampled observation or a whole-clip property? That distinction determines which files it can safely accept.&lt;/p&gt;




&lt;p&gt;Project context: this inspector is maintained alongside &lt;a href="https://bgremover.online/" rel="noopener noreferrer"&gt;BG Remover Online&lt;/a&gt;. It neither removes backgrounds nor uploads files, and its production integration into that site is pending.&lt;/p&gt;

&lt;p&gt;AI disclosure: this article was drafted with AI. The synthetic fixtures, pixel inspections, assertions, and four CLI exit-code cases were executed against the recorded commit on October 23, 2026.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>javascript</category>
      <category>testing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>How to Encode and Decode Mermaid Share Links in JavaScript</title>
      <dc:creator>river</dc:creator>
      <pubDate>Sun, 25 Oct 2026 03:32:47 +0000</pubDate>
      <link>https://dev.to/river_jiang_670cccce7d97d/how-to-encode-and-decode-mermaid-share-links-in-javascript-2ppc</link>
      <guid>https://dev.to/river_jiang_670cccce7d97d/how-to-encode-and-decode-mermaid-share-links-in-javascript-2ppc</guid>
      <description>&lt;p&gt;A teammate sends you an architecture diagram. You can read it, but the next request is to rename a service and change a route. A screenshot has already discarded the useful part: the source.&lt;/p&gt;

&lt;p&gt;Here is the result worth preserving: a rendered API gateway diagram with services, databases, and relationships. A share link can carry the Mermaid source so the receiving editor can render—and edit—it again.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu770wjp7jtkc3ppf0sdd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fu770wjp7jtkc3ppf0sdd.png" alt="Actual rendered API gateway diagram with 11 nodes, backed by a verified 715-byte source round trip." width="800" height="476"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Actual Mermaid Online rendering of the separate visual example below. The codec preserved its 715 UTF-8 source bytes exactly; rendering is the editor’s job. The small Unicode test later in this article is a different, 57-byte input.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mermaidonline.org/#pako:eNp1kl1r2zAUhv-KUG42cNnsxA7zxaCpYRt0LLQdu6h3cSwdxaJyNGQ5JbT975Nky1lGeyUf-XnPq_PxRDs0HUhOS_pUU9tihzUta8pRwKBsTV9oQoc_HCxWEnYGOlpaM2BCjR52LS0FqN5FTHN0OYTSj6wFY8n1Tb0n5EpJ3Nv7mv7Chnwg33UjlTP4TS4uPj9_vbvb3j6TLy73IxwddLn9FiPHeP0UeZxcDrb1kDvIVYvs4RXmWnbS2924uzGYoKDyxM8ejQP8QW7RHCTz7zlDfhgemHC-BW2kUnK_c9j09R8YDKJhtbl_N3lWm5q-D8SYfjYcmfHyBMXkHtsafZDj07Zw7Fxj56vJNFQ8NkLvel9C4zwP4LoubewpU9D3FQriEpgjEc6hXPAGQWDSW6MfsFxkebHEJmFaaVMu0nWW56szLdMGJyly_PSPdM2WgDxKM0w_FvmZ1O0SRFcmGK5naVrAcgWz6ypfZvxMKkCqYTYWiBlms5qzrMiKqF6LlKcn9bSJSVyWUPvprx9r4ueThAEkse2-zBM1jnJEqk0opN7Tl79GZBSP" rel="noopener noreferrer"&gt;Open the visual example in the editor&lt;/a&gt;. Its source is included here, so the picture does not have to be taken on trust.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Open the Mermaid source used for the rendered architecture
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  Client["Web / Mobile"] --&amp;gt;|HTTPS| Gateway["API Gateway"]
  Gateway --&amp;gt; Auth["Auth Check"]
  Gateway --&amp;gt; Limit["Rate Limit"]
  Auth --&amp;gt; User["User Service"]
  Auth --&amp;gt; Order["Order Service"]
  Auth --&amp;gt; Billing["Billing Service"]
  User --&amp;gt; UserDB[("User DB")]
  Order --&amp;gt; OrderDB[("Order DB")]
  Billing --&amp;gt; Provider["Payment Provider"]
  Limit --&amp;gt; Logs["Observability"]
  classDef entry fill:#dbeafe,stroke:#2563eb,color:#172554
  classDef core fill:#ede9fe,stroke:#7c3aed,color:#2e1065
  classDef data fill:#dcfce7,stroke:#16a34a,color:#14532d
  classDef failure fill:#fee2e2,stroke:#dc2626,color:#7f1d1d
  class Client,Gateway entry
  class Auth,User,Order,Billing core
  class UserDB,OrderDB data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The smaller executable test below then concentrates on the failure-prone details: Chinese text, an emoji, CRLF line endings, configuration, malformed input, and a decoded-size limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the fragment contains
&lt;/h2&gt;

&lt;p&gt;For the &lt;code&gt;pako:&lt;/code&gt; format covered here, the pipeline is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;state object
  → JSON
  → UTF-8 bytes
  → zlib-wrapped DEFLATE
  → Base64URL
  → #pako:...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The compressed data is a document, not an image. A minimal state might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"flowchart LR&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;  A--&amp;gt;B"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mermaid"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;theme&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;dark&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;}"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice that &lt;code&gt;mermaid&lt;/code&gt; is a &lt;strong&gt;JSON string&lt;/strong&gt;, rather than a nested configuration object. That detail follows the &lt;a href="https://github.com/mermaid-js/mermaid-live-editor/blob/eb4141dd51247259e95ecd890c1bf3dabf314913/src/lib/util/serde.ts" rel="noopener noreferrer"&gt;recorded Mermaid Live Editor serializer&lt;/a&gt; used as a compatibility reference.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;pako:&lt;/code&gt; prefix names the format. An implementation can use a different compatible compression library.&lt;/p&gt;

&lt;p&gt;There is also an uncompressed &lt;code&gt;base64:&lt;/code&gt; variant. A cloud document ID, a site-specific &lt;code&gt;#code=&lt;/code&gt; parameter, and an arbitrary Base64 string are different inputs; a decoder should not silently guess that they follow this format.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wxkvcph2j4k9bqs6h3r.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wxkvcph2j4k9bqs6h3r.png" alt="Five-stage Mermaid share format: state, UTF-8 JSON, zlib, Base64URL, and URL fragment." width="799" height="344"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Each boundary has a different job. The diagram travels as serialized data, without loading a Mermaid renderer.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the example from a source checkout
&lt;/h2&gt;

&lt;p&gt;The example uses &lt;a href="https://github.com/j-river/mermaid-share-codec" rel="noopener noreferrer"&gt;mermaid-share-codec&lt;/a&gt;, an independent MIT project. npm publication is pending, so these instructions use a recorded Git commit rather than a registry install.&lt;/p&gt;

&lt;p&gt;Use Node.js 22 or newer. The run reported below used Node.js 24.13.1 on Windows.&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 https://github.com/j-river/mermaid-share-codec.git
&lt;span class="nb"&gt;cd &lt;/span&gt;mermaid-share-codec
git checkout d3d7c90eab4e5e7cb0fa22dca184e937b3f71d28
npm ci &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt;
npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the following as &lt;code&gt;roundtrip.mjs&lt;/code&gt; in that repository's root, then run &lt;code&gt;node roundtrip.mjs&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;/p&gt;
  Open the complete runnable round-trip and failure checks
  &lt;br&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="nx"&gt;assert&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;node:assert/strict&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;deflateSync&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;node:zlib&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;createShareUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;extractCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decodeState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;encodeState&lt;/span&gt;&lt;span class="p"&gt;,&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;./dist/index.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;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;flowchart LR&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s1"&gt;  A["开始 🚀"] --&amp;gt; B["Review &amp;amp; ship"]&lt;/span&gt;&lt;span class="se"&gt;\r\n&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;mermaid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&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;baseUrl&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://mermaid.live/edit&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;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createShareUrl&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mermaid&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;baseUrl&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;recovered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extractCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recovered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;recovered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Includes CRLF and the trailing newline.&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decodeState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&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="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mermaid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mermaid&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;origin&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;baseUrl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;origin&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&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;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/edit&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// A second decoder path: use Node's compressor to produce an input token.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;independent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pako:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;deflateSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;mermaid&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&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;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64url&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;extractCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;independent&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;code&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;malformed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decodeState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pako:!!!!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;malformed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;malformed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INVALID_BASE64&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// A small compressed input can expand beyond the application's limit.&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;oversized&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;pako:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;deflateSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;code&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;repeat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}))).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;base64url&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;bounded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decodeState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;oversized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;maxDecodedBytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;_024&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bounded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;bounded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;OUTPUT_TOO_LARGE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;throws&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;encodeState&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;mermaid&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;theme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dark&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="nx"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INVALID_STATE&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="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;roundTrip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;recovered&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;configRoundTrip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;mermaid&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;mermaid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;independentlyCompressedInput&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;malformedError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;malformed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;boundedError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;bounded&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;urlCharacters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sourceUtf8Bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Buffer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;byteLength&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&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="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;&lt;/p&gt;

&lt;p&gt;The first assertions compare the recovered &lt;strong&gt;source string&lt;/strong&gt;, including its CRLF line endings and final newline. A screenshot comparison would not catch those differences.&lt;/p&gt;

&lt;p&gt;The configuration is checked separately. The URL's origin and path are also asserted, so the test verifies the selected destination as well as the payload.&lt;/p&gt;

&lt;p&gt;The independently compressed token uses Node's &lt;code&gt;zlib&lt;/code&gt;, rather than the codec's encoder. This checks one external input path instead of relying exclusively on an encoder and decoder agreeing with each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually passed
&lt;/h2&gt;

&lt;p&gt;The example was executed on October 23, 2026. Its output was:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"roundTrip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"configRoundTrip"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"independentlyCompressedInput"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"malformedError"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"INVALID_BASE64"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"boundedError"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"OUTPUT_TOO_LARGE"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"urlCharacters"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;225&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceUtf8Bytes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;57&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those size values describe this particular source and encoded state. A tiny diagram can produce a URL longer than its source because JSON fields, compression headers, the prefix, and the destination add overhead. Compression is not a promise that every input becomes smaller.&lt;/p&gt;

&lt;p&gt;Do not snapshot the entire compressed URL as the primary compatibility test. Two compressors can emit different streams that recover the same state. Compare the decoded fields, and keep a separately sourced editor fixture or serializer test as an interoperability check.&lt;/p&gt;

&lt;p&gt;This experiment verifies source and configuration recovery. It does not test whether the Mermaid syntax renders, or whether every target editor accepts every configuration field.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc5ecks5yid0klktl0whz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc5ecks5yid0klktl0whz.png" alt="Three actual outcomes: exact round trip passes, malformed Base64 is rejected, and decoded output beyond 1024 bytes is rejected." width="800" height="361"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;These are the error outcomes from the runnable example, not estimated robustness scores.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Make decoder limits part of the feature
&lt;/h2&gt;

&lt;p&gt;The malformed input returns &lt;code&gt;INVALID_BASE64&lt;/code&gt;. The second failure is more interesting: the token represents 20,000 repeated characters, but decoding is restricted to 1,024 uncompressed bytes.&lt;/p&gt;

&lt;p&gt;Checking only the pasted URL length would miss the expansion. The decoder returns &lt;code&gt;OUTPUT_TOO_LARGE&lt;/code&gt; while enforcing its output bound.&lt;/p&gt;

&lt;p&gt;A sensible boundary has several parts:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Boundary&lt;/th&gt;
&lt;th&gt;What it controls&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Encoded input length&lt;/td&gt;
&lt;td&gt;How much data a caller can submit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Uncompressed byte limit&lt;/td&gt;
&lt;td&gt;How much decoded JSON can be produced&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JSON depth&lt;/td&gt;
&lt;td&gt;How deeply the resulting data can nest&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Application policy&lt;/td&gt;
&lt;td&gt;Which fields the editor actually accepts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For this codec, callers can lower the built-in ceilings. They cannot raise them arbitrarily. The defaults and error codes are documented in the repository.&lt;/p&gt;

&lt;p&gt;These limits reduce resource use. Synchronous decompression still consumes CPU; it is not a general-purpose isolation boundary. Keep that distinction when adapting the example to an application that handles untrusted links.&lt;/p&gt;

&lt;h2&gt;
  
  
  Recovering data does not authorize applying it
&lt;/h2&gt;

&lt;p&gt;A decoded configuration can contain settings your application should not accept.&lt;/p&gt;

&lt;p&gt;For a source-recovery feature, extracting only &lt;code&gt;code&lt;/code&gt; is often enough. If the application also supports configuration import, define an allowlist of supported options rather than merging arbitrary decoded fields into the renderer.&lt;/p&gt;

&lt;p&gt;Likewise, display recovered code with a textarea or &lt;code&gt;textContent&lt;/code&gt;. A successful JSON decode says nothing about whether text is safe to insert as HTML.&lt;/p&gt;

&lt;p&gt;In an interactive editor, source edits should also invalidate any previously generated link. Otherwise, a user can change a label and copy a link that still contains the old diagram.&lt;/p&gt;

&lt;h2&gt;
  
  
  A fragment keeps the payload out of one request, not out of reach
&lt;/h2&gt;

&lt;p&gt;The browser does not include the fragment in the HTTP request for the document. It processes that part of the URL on the client. &lt;a href="https://developer.mozilla.org/en-US/docs/Web/URI/Reference/Fragment" rel="noopener noreferrer"&gt;MDN's fragment reference&lt;/a&gt; describes this behavior.&lt;/p&gt;

&lt;p&gt;The destination page's scripts can still read the fragment. So can anyone who receives the complete link. Compression does not encrypt the source, and a self-contained link has no central record to revoke.&lt;/p&gt;

&lt;p&gt;For confidential diagrams, choose a sharing mechanism with the access and revocation behavior the project requires. Also test the real delivery path: a browser, chat application, or receiving editor can impose practical URL-length limits below the codec's own limit.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F72gqiq2kmji9g5nbaubq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F72gqiq2kmji9g5nbaubq.png" alt="Actual Mermaid Online browser editor showing the architecture source and rendered preview together." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Actual browser editor capture. The visual example was edited in the source panel and rendered successfully. The independent share-codec playground linked below exposes recovered JSON; it does not render diagrams.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Try a changed input
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://huggingface.co/spaces/j-river/mermaid-share-codec" rel="noopener noreferrer"&gt;browser playground&lt;/a&gt; exposes the source, generated link, and recovered JSON without a local setup. It performs encoding and decoding in browser memory; it does not render the diagram.&lt;/p&gt;

&lt;p&gt;A useful next experiment is to alter only the final newline, or replace an English label with Chinese and an emoji. Check source equality after decoding. Then lower the decoded-byte limit and check that failure remains an explicit error rather than an empty diagram.&lt;/p&gt;

&lt;p&gt;For editors that already import shared state: which settings do you deliberately refuse to import, and how do you explain that choice in the UI?&lt;/p&gt;




&lt;p&gt;Project context: this standalone codec is maintained alongside &lt;a href="https://mermaidonline.org/" rel="noopener noreferrer"&gt;Mermaid Online&lt;/a&gt;. It is independent of the official Mermaid project, and the extracted package's production integration is pending.&lt;/p&gt;

&lt;p&gt;AI disclosure: this article was drafted with AI. The included executable example was run against the recorded source commit; the measured output above is from that run.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Edited Text in an AI Poster—Then Checked What Else Changed</title>
      <dc:creator>river</dc:creator>
      <pubDate>Wed, 14 Oct 2026 09:32:00 +0000</pubDate>
      <link>https://dev.to/river_jiang_670cccce7d97d/i-edited-text-in-an-ai-poster-then-checked-what-else-changed-37l0</link>
      <guid>https://dev.to/river_jiang_670cccce7d97d/i-edited-text-in-an-ai-poster-then-checked-what-else-changed-37l0</guid>
      <description>&lt;p&gt;The moon was right. The colors were right. Even the tiny event details were right. The headline was the one thing that needed to change: &lt;strong&gt;NIGHT BLOOM&lt;/strong&gt; had to become &lt;strong&gt;AFTER DARK&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This is a controlled poster example from our EditableImage text-editing workflow, not a client campaign. Regenerating the entire frame would throw away the parts we wanted to keep. So the useful question wasn't “Can AI write the new words?” It was: &lt;strong&gt;Can we change those words without quietly changing everything around them?&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Before: NIGHT BLOOM&lt;/th&gt;
&lt;th&gt;After: AFTER DARK&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fopq8zz6m1bu8wblozkty.webp" alt="Original purple jazz poster with NIGHT BLOOM as its headline" width="800" height="450"&gt;&lt;/td&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F67bq07klvuhxmevt0agg.webp" alt="The same poster with AFTER DARK as its headline" width="800" height="450"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;em&gt;Only the headline was meant to change. The moon, vines, gradient, and small event details were meant to stay put.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The test I care about is outside the edit
&lt;/h2&gt;

&lt;p&gt;A side-by-side comparison can miss subtle drift. A model might make the moon slightly smaller, move a leaf, or rewrite the event time while fixing the headline. Those are failures when the surrounding design is the part you wanted to preserve.&lt;/p&gt;

&lt;p&gt;For this test, we marked the headline, generated replacement lettering, and composited only the marked result onto the original image. We then compared the two versions pixel by pixel. The image below is an &lt;strong&gt;annotated change map, not the finished poster&lt;/strong&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzt01bnraaxxizmssvm11.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzt01bnraaxxizmssvm11.webp" alt="Annotated change map with orange old NIGHT BLOOM lettering and blue new AFTER DARK lettering overlapping in the headline area" width="800" height="395"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Orange marks the old “NIGHT BLOOM” lettering; blue marks the new “AFTER DARK” lettering. The two phrases overlap because they occupy the same part of the poster, so this map is not meant to be read as a headline. The dimmed background is shown only for reference.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On the web-compressed examples above, the meaningful differences occupy &lt;strong&gt;under 4% of the frame&lt;/strong&gt;, all in the headline area. The comparison ignores faint compression noise (up to 22/255 here). The downloadable editor result is a lossless PNG; outside the selected lines, it retains the original pixels. The 4% figure describes this poster, not a promise for every image.&lt;/p&gt;

&lt;p&gt;That distinction matters. “Looks similar” is subjective. “Nothing outside the marked area changed” is a property you can check.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical check for your own image
&lt;/h2&gt;

&lt;p&gt;You don't need a special tool to apply the same reasoning:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Keep the original file.&lt;/strong&gt; Compare against that file, not a screenshot of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Align dimensions.&lt;/strong&gt; If the output was resized or cropped, a pixel diff will light up everywhere and tell you little.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mark the allowed region.&lt;/strong&gt; Decide &lt;em&gt;before editing&lt;/em&gt; which pixels may change. For this poster, it's the headline; for a product screenshot, it might be one button label.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Look outside that region.&lt;/strong&gt; Any substantial change there deserves an explanation. If you're comparing JPEG or WebP copies, first allow for compression noise; use lossless exports when you can.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zoom in at 100%.&lt;/strong&gt; The diff can tell you where pixels changed, but not whether the new lettering has a clean edge or a believable font.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The same test is especially useful for an approved UI screenshot with an outdated button label or a campaign image with last month's date. The design around the words is not disposable just because the words are wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the result can still disappoint
&lt;/h2&gt;

&lt;p&gt;Keeping the original outside the marked lines creates a trade-off at the boundary. On a smooth gradient like this poster, the seam is hard to notice. On a photograph, grain, or a busy pattern, the newly inferred background behind old letters may not join perfectly with the original. A full-image redraw can hide that seam, but it can also shift details elsewhere. Neither mode is universally better; compare both at full resolution.&lt;/p&gt;

&lt;p&gt;This also doesn't recover an original font file. The new lettering is drawn to resemble the surrounding type; an exact font match isn't guaranteed. If the lettering &lt;em&gt;is&lt;/em&gt; the artwork—a hand-painted logo, for example—I would rebuild it manually rather than expect a text edit to preserve its character.&lt;/p&gt;

&lt;p&gt;My takeaway from this poster isn't that AI can “fix any image.” It's a narrower, more useful rule: &lt;strong&gt;when one part of an image is wrong, define the permitted change and inspect everything outside it.&lt;/strong&gt; That turns a plausible-looking edit into one you can actually evaluate.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://editableimage.com/guides/fix-text-in-an-ai-generated-image" rel="noopener noreferrer"&gt;full poster case and its limitations&lt;/a&gt; are on our site. If you want to try the same bounded edit on your own asset, here's the &lt;a href="https://editableimage.com/edit-text-in-image" rel="noopener noreferrer"&gt;text-editing workflow&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>design</category>
      <category>tutorial</category>
      <category>tools</category>
    </item>
    <item>
      <title>The Mermaid Flowchart Rendered—but the Failure Path Was Missing</title>
      <dc:creator>river</dc:creator>
      <pubDate>Wed, 14 Oct 2026 03:38:31 +0000</pubDate>
      <link>https://dev.to/river_jiang_670cccce7d97d/the-mermaid-flowchart-rendered-but-the-failure-path-was-missing-24e3</link>
      <guid>https://dev.to/river_jiang_670cccce7d97d/the-mermaid-flowchart-rendered-but-the-failure-path-was-missing-24e3</guid>
      <description>&lt;p&gt;A deployment flowchart went from a red parser error to a clean, exportable diagram—but the most important fix was not syntax. Mermaid Online identified two concrete code errors; the preview then exposed a missing failure path. Below is the exact before-and-after, with screenshots and the code you can adapt.&lt;/p&gt;

&lt;p&gt;This is a small, reproducible walkthrough, not a production incident. The point is to show where the tool saves time and where a human still has to decide what the arrows mean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The starting draft
&lt;/h2&gt;

&lt;p&gt;The intended flow was simple: push to main, build an image, run smoke tests, then deploy only if they pass. Here is the deliberately flawed first version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A[Push to main] --&amp;gt; B[Build image (v2.4)]
  B --&amp;gt; C{Smoke tests pass?} C --&amp;gt;|yes| D[Deploy to prod (us-west)]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pasting this into the &lt;a href="https://mermaidonline.org/mermaid-syntax-checker" rel="noopener noreferrer"&gt;Mermaid syntax checker&lt;/a&gt; produced a parse error on line 2. The opening parenthesis in “Build image (v2.4)” was being read as Mermaid syntax rather than label text. The smallest repair was to quote the label:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A["Push to main"] --&amp;gt; B["Build image (v2.4)"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That did not finish the job. The next check reached line 3 and stopped at the second C: two statements had been squeezed onto one line. Its unexpected-token clue was NODE_STRING. Splitting the edge and the yes branch onto separate lines fixed that error.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F70pr7qnjryuvo5igs809.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F70pr7qnjryuvo5igs809.webp" alt="Mermaid syntax checker showing a line-level parse error and expected tokens" width="799" height="454"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Example of the checker's line-level feedback. The full code for this walkthrough is shown above and below.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The useful lesson is that the first parser error is not necessarily the only one. Fix the reported line, run the parser again, and keep the rest of the diagram intact. A wholesale rewrite can accidentally change the process you meant to document.&lt;/p&gt;

&lt;h2&gt;
  
  
  A green check was not the finish line
&lt;/h2&gt;

&lt;p&gt;Once the code parsed, the live preview made the larger problem obvious. The decision diamond had a yes route to production and no route for a failed smoke test. That is not a Mermaid error. A parser cannot infer your release policy.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5u2x73auzl1f3ekkdrqs.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5u2x73auzl1f3ekkdrqs.webp" alt="A syntax-valid deployment flowchart in the Mermaid Online editor, showing only the passing branch" width="800" height="584"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;An earlier preview of the yes-only version: syntactically valid, but the failure path is absent.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For this example, a failed test should send the team to investigate and push a fix before the build runs again. The diagram also needs to show what happens after deployment. Here is the complete tested version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A["Push to main"] --&amp;gt; B["Build image (v2.4)"]
  B --&amp;gt; C{"Smoke tests pass?"}
  C --&amp;gt;|yes| D["Deploy to prod (us-west)"]
  C --&amp;gt;|no| E["Investigate and push fix"]
  E --&amp;gt; A
  D --&amp;gt; F["Monitor"]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvjt9watat6ycmvlzaiwj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvjt9watat6ycmvlzaiwj.png" alt="Final Mermaid deployment flowchart: smoke tests branch to production and monitoring on yes, or investigation and a new push on no" width="799" height="219"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The final diagram exported from the exact code above. The &lt;code&gt;no&lt;/code&gt; route returns through a fix and a new push; the &lt;code&gt;yes&lt;/code&gt; route continues to deployment and monitoring.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;It passes the checker and renders with both branches visible. The return arrow goes to &lt;strong&gt;Push to main&lt;/strong&gt;, not straight back to &lt;strong&gt;Build image&lt;/strong&gt;: this example assumes a new code change is needed before retrying. If your team reruns a flaky test without a new commit, that arrow should be different. The diagram is useful only if the arrows describe the real procedure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting it into the document
&lt;/h2&gt;

&lt;p&gt;A valid preview is still not the same thing as a deliverable. For this case, the &lt;a href="https://mermaidonline.org/#tool" rel="noopener noreferrer"&gt;Mermaid Online editor’s export panel&lt;/a&gt; produced a 2× PNG in the Clean Docs standard style. The page reported a 2452 × 672 px file with no watermark. The same panel offers SVG, PDF, and other image formats; the core checker and standard-style exports do not require an account.&lt;/p&gt;

&lt;p&gt;The destination decides what to keep:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;In a GitHub README that supports Mermaid, keep the source code. Reviewers can diff a changed edge and future editors can fix a label without recreating an image.&lt;/li&gt;
&lt;li&gt;In a slide deck or document that will not render Mermaid, use SVG when it is supported or a high-resolution PNG when compatibility matters. Keep the source beside the exported asset so the image does not become the only copy of the diagram.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The layout choice matters too. A long pipeline can become an awkward vertical strip with top-down direction. Left-to-right worked better for this example, but it made the exported image wide. A slide may need fewer nodes or two smaller diagrams rather than a tiny unreadable one.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fke8d6miq542q3ng94xyl.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fke8d6miq542q3ng94xyl.webp" alt="The same pipeline compared in top-down and left-to-right Mermaid layouts" width="800" height="954"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The same steps can read very differently depending on direction and where the diagram will live.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is the workflow Mermaid Online makes less fiddly: paste code, get a concrete parser error, inspect the rendered flow, and export a clean asset. It shortened the mechanical part of this example. It did not decide that the no branch was missing, or whether a failed test should lead to a retry, rollback, or manual review. Those remain the author's decisions.&lt;/p&gt;

&lt;p&gt;The next time a generated diagram gives you a red banner, fix the first reported token. When it turns green, ask the harder question: &lt;em&gt;what would a teammate misunderstand if they used this diagram to do the job?&lt;/em&gt; For more on prompt constraints and other Mermaid failure modes, there is a &lt;a href="https://mermaidonline.org/blog/ai-mermaid-diagrams" rel="noopener noreferrer"&gt;longer guide here&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>mermaid</category>
      <category>ai</category>
      <category>debugging</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A Checkerboard Is Not Transparency: A Practical Alpha Test for Video</title>
      <dc:creator>river</dc:creator>
      <pubDate>Fri, 09 Oct 2026 07:24:06 +0000</pubDate>
      <link>https://dev.to/river_jiang_670cccce7d97d/a-checkerboard-is-not-transparency-a-practical-alpha-test-for-video-4dk2</link>
      <guid>https://dev.to/river_jiang_670cccce7d97d/a-checkerboard-is-not-transparency-a-practical-alpha-test-for-video-4dk2</guid>
      <description>&lt;p&gt;You export a “background-free” video, put it on a web page, and see a black rectangle. Is the file broken? Perhaps. But the player might be ignoring a real alpha channel.&lt;/p&gt;

&lt;p&gt;Before running background removal again, separate three things: &lt;strong&gt;the preview background&lt;/strong&gt;, &lt;strong&gt;the file's alpha channel&lt;/strong&gt;, and &lt;strong&gt;the software playing the file&lt;/strong&gt;. They can produce similar-looking screenshots for very different reasons.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a no-code test
&lt;/h2&gt;

&lt;p&gt;Place the &lt;em&gt;exported file&lt;/em&gt; above a bright, solid-color layer in a video editor that supports transparency. Change that layer to a very different color. Watch the space around the subject, then pause on frames with hair, hands, or motion blur.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If both colors show through around the subject, the file is being composited with transparency in that editor.&lt;/li&gt;
&lt;li&gt;If you see a fixed checkerboard, black, or white rectangle, either those pixels are baked into the file &lt;strong&gt;or&lt;/strong&gt; the current playback path is not displaying alpha. Try a second known-compatible destination before blaming the removal step.&lt;/li&gt;
&lt;li&gt;If the background changes but the edges look poor, alpha is present; your problem is cutout quality, not format support.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This visual test tells you what the &lt;em&gt;destination&lt;/em&gt; can display. It does not prove every application will handle the same file in the same way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the checkerboard actually means
&lt;/h2&gt;

&lt;p&gt;The grey-and-white checkerboard is a visual convention used by editors to represent empty space. It is not transparency itself. If those squares are written into the exported video, you have an opaque video of a checkerboard.&lt;/p&gt;

&lt;p&gt;An alpha channel stores opacity alongside color. At one pixel, alpha &lt;code&gt;0&lt;/code&gt; means the background can show through; alpha &lt;code&gt;1&lt;/code&gt; means the foreground is fully opaque. Values in between let hair, glass, or motion-blurred edges blend with whatever is underneath. Apple’s &lt;a href="https://www.apple.com/support/assets/docs/products/finalcutpro/Apple_ProRes_June_2014.pdf" rel="noopener noreferrer"&gt;ProRes white paper&lt;/a&gt; describes alpha as the data used to composite a foreground image over a background.&lt;/p&gt;

&lt;p&gt;That is why the test uses &lt;strong&gt;two contrasting backgrounds and the actual exported file&lt;/strong&gt;, rather than accepting one preview screenshot as proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  A tiny browser test for VP9-alpha WebM
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;alpha-test.html&lt;/code&gt; next to your exported &lt;code&gt;cutout.webm&lt;/code&gt;, then open it in a browser that supports VP9 alpha. Replace the filename if yours is different.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"stage"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"stage"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;video&lt;/span&gt; &lt;span class="na"&gt;autoplay&lt;/span&gt; &lt;span class="na"&gt;muted&lt;/span&gt; &lt;span class="na"&gt;loop&lt;/span&gt; &lt;span class="na"&gt;playsinline&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;source&lt;/span&gt; &lt;span class="na"&gt;src=&lt;/span&gt;&lt;span class="s"&gt;"cutout.webm"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"video/webm"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/video&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"switch-bg"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Switch background&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;style&amp;gt;&lt;/span&gt;
  &lt;span class="nc"&gt;.stage&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#14223b&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;720px&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.stage.light&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;#f2e8d5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nc"&gt;.stage&lt;/span&gt; &lt;span class="nt"&gt;video&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;display&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;block&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;width&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100%&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/style&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stage&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;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#stage&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&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;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#switch-bg&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&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="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;stage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;classList&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toggle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;light&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="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the area &lt;em&gt;around&lt;/em&gt; the subject switches from navy to cream, this browser is compositing the video over the page. Inspect the edges as well as the empty areas. If a solid rectangle remains, the file may be opaque—or the browser may not support this alpha path. Test in another alpha-aware destination before re-exporting. MDN's &lt;a href="https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats/Video_codecs" rel="noopener noreferrer"&gt;video codec guide&lt;/a&gt; notes that Safari does not support alpha transparency in VP9.&lt;/p&gt;

&lt;p&gt;If the video does not play at all, check that the filename, path, and codec are correct before interpreting the result as an alpha failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a real transparent background matters
&lt;/h2&gt;

&lt;p&gt;A cutout with a working alpha channel is reusable footage, not just a video that looks clean against one chosen color. You can place the same person or object over different website sections, slide backgrounds, or editing timelines without removing the background again for each design. That is especially useful when a campaign's colors change after the footage has already been prepared.&lt;/p&gt;

&lt;p&gt;The practical advantage is flexibility: export once in a format your destination supports, then composite the subject over the background that fits the project. A baked-in checkerboard or solid-color rectangle takes that flexibility away.&lt;/p&gt;

&lt;h2&gt;
  
  
  A preview of the workflow—and what to verify
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://bgremover.online/" rel="noopener noreferrer"&gt;BG Remover Online example&lt;/a&gt; shows a processed 3.75-second, 1920×1080 clip. The page identifies its 90-frame output as VP9-alpha WebM, and lets you switch the &lt;em&gt;preview&lt;/em&gt; among checkerboard, blue, and dark backgrounds. You can try those controls without uploading anything. Seeing the same cutout on contrasting backgrounds helps you judge whether its edges will work in more than one design—not just whether it looks good on a checkerboard.&lt;/p&gt;

&lt;p&gt;A separate outdoor result makes the comparison easier to see. This Product Hunt gallery image uses the same processed frame against three &lt;em&gt;preview backgrounds&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fag0cw8u25r7lh726lvf0.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fag0cw8u25r7lh726lvf0.jpg" alt="One BG Remover cutout previewed on checkerboard, blue, and dark backgrounds" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Source footage: &lt;a href="https://www.pexels.com/video/5972688/" rel="noopener noreferrer"&gt;Pexels&lt;/a&gt;. These are preview composites, not three different exports or proof about the downloaded file.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Both demonstrations are still site previews. The cover image shows its original frame next to the cutout on a checkerboard; &lt;strong&gt;the cover alone cannot verify the alpha channel in a downloaded file&lt;/strong&gt;. For your own project, export your clip and run the two-background test above in the destination that matters to you. The &lt;a href="https://mixkit.co/free-stock-video/bearded-announcer-close-up-2960/" rel="noopener noreferrer"&gt;source footage is from Mixkit&lt;/a&gt;; the person shown is not affiliated with the product.&lt;/p&gt;

&lt;h2&gt;
  
  
  When a “transparent” file looks black
&lt;/h2&gt;

&lt;p&gt;I diagnose the problem in this order:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Check the actual output format.&lt;/strong&gt; An ordinary H.264 MP4 is not an alpha-delivery file for this workflow. Renaming &lt;code&gt;.mp4&lt;/code&gt; to &lt;code&gt;.webm&lt;/code&gt; does not add opacity data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test a known alpha-aware editor or browser.&lt;/strong&gt; If the background layer shows through there, your original player is the likely limitation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Switch between light and dark backgrounds.&lt;/strong&gt; One color may conceal halos, missing hair, or a leftover matte.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Inspect multiple frames.&lt;/strong&gt; A clean still can shimmer when the subject moves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Only then re-export.&lt;/strong&gt; If the file fails in more than one compatible destination, choose an alpha-capable export and verify the new file before delivery.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Pick the format for the destination
&lt;/h2&gt;

&lt;p&gt;There is no single transparent-video format that is ideal everywhere.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Destination&lt;/th&gt;
&lt;th&gt;Practical choice&lt;/th&gt;
&lt;th&gt;Caveat&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supported web playback and overlays&lt;/td&gt;
&lt;td&gt;WebM with VP9 alpha&lt;/td&gt;
&lt;td&gt;Test the target browser; Safari does not display VP9 alpha.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Editing and compositing&lt;/td&gt;
&lt;td&gt;ProRes 4444&lt;/td&gt;
&lt;td&gt;Large files; an alpha channel is supported but must actually be present in the export.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compatible Apple playback&lt;/td&gt;
&lt;td&gt;HEVC with alpha&lt;/td&gt;
&lt;td&gt;A distinct alpha-capable HEVC workflow, not an ordinary MP4 export.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Apple documents &lt;a href="https://www.apple.com/support/assets/docs/products/finalcutpro/Apple_ProRes_June_2014.pdf" rel="noopener noreferrer"&gt;ProRes 4444 alpha support&lt;/a&gt; and &lt;a href="https://developer.apple.com/videos/play/wwdc2019/506/" rel="noopener noreferrer"&gt;HEVC with alpha&lt;/a&gt;. Choose based on where the file will be used, then verify the &lt;em&gt;actual output&lt;/em&gt;, not only the codec name.&lt;/p&gt;

&lt;p&gt;If you use BG Remover Online for that test, its current limits are worth knowing first: video uploads are capped at 60 seconds; it exports VP9-alpha WebM, and offers ProRes 4444 for processed clips up to 15 seconds. It does not currently export HEVC alpha or retain the original audio. Google sign-in gives new accounts 5 free credits—enough for five images or up to five started seconds of video, with no card. Uploads are private: an upload you never process is deleted after 24 hours, and finished results are kept for 7 days and can be deleted sooner. Check the current terms before uploading sensitive footage.&lt;/p&gt;

&lt;p&gt;If you need a subject you can reuse over different backgrounds, &lt;a href="https://bgremover.online/" rel="noopener noreferrer"&gt;try BG Remover Online with a short, non-sensitive clip&lt;/a&gt;. Switch the preview background to inspect the edges, download the transparent video, then test that file in your actual editor or browser. The result is valuable when it saves a reshoot or a second cutout—not merely when the preview looks convincing.&lt;/p&gt;

&lt;p&gt;The general rule is tool-independent: &lt;strong&gt;a checkerboard preview is a prompt to test, not evidence that the file will composite where you need it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built BG Remover Online. AI assistance was used to draft and edit this article; product-specific details were checked against its public pages. The public preview is described as a preview, not as an independent inspection of the downloaded file.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>css</category>
      <category>showdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A PPTX Is Not Necessarily an Editable Slide: Five Tests That Matter</title>
      <dc:creator>river</dc:creator>
      <pubDate>Fri, 09 Oct 2026 07:22:43 +0000</pubDate>
      <link>https://dev.to/river_jiang_670cccce7d97d/a-pptx-is-not-necessarily-an-editable-slide-five-tests-that-matter-19ig</link>
      <guid>https://dev.to/river_jiang_670cccce7d97d/a-pptx-is-not-necessarily-an-editable-slide-five-tests-that-matter-19ig</guid>
      <description>&lt;p&gt;A screenshot pasted onto a PowerPoint slide is technically a &lt;code&gt;.pptx&lt;/code&gt;. It is also still just a screenshot. You can resize it, but you cannot rewrite the title, move a KPI card, or correct a number without rebuilding part of the slide.&lt;/p&gt;

&lt;p&gt;That distinction matters when the original deck has disappeared and the only surviving copy is a PNG in a wiki, a PDF export, or a screenshot in a chat thread. The useful question is not &lt;strong&gt;“Can I get a PPTX?”&lt;/strong&gt; It is &lt;strong&gt;“Which edits will the downloaded PPTX actually let me make?”&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here is a five-part test you can run on any image-to-slide workflow. The point is to test the exported file, not just trust a polished preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Can you change the words as words?
&lt;/h2&gt;

&lt;p&gt;Click the main title and replace one word. Then try a small label, such as a KPI value or footnote.&lt;/p&gt;

&lt;p&gt;If the text is a native text box, you should be able to select characters and change the copy. If the output merely puts a new text box over old text baked into the background, the old letters may reappear when the replacement is shorter. Move the text box temporarily and inspect what is underneath.&lt;/p&gt;

&lt;p&gt;Typography is a second test. A converter may recognize the correct words but substitute a font, weight, or line break. That is recoverable, but it is still review work.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Do the pieces move independently?
&lt;/h2&gt;

&lt;p&gt;Try moving the title, one card, and the chart region. If the whole slide moves together, the file is mostly an image inside a PPTX wrapper.&lt;/p&gt;

&lt;p&gt;There is an important distinction inside this test: a chart that moves as one graphic is &lt;strong&gt;not necessarily&lt;/strong&gt; a native PowerPoint chart with an editable data table. Likewise, a recovered icon might be an independent image rather than a vector shape. Decide what level of editing you need before judging the export:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What you need to do&lt;/th&gt;
&lt;th&gt;Minimum useful result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rewrite a heading&lt;/td&gt;
&lt;td&gt;Native text box&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reposition a visual&lt;/td&gt;
&lt;td&gt;Separate slide object&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recolor individual parts&lt;/td&gt;
&lt;td&gt;Editable shapes or layers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change chart values&lt;/td&gt;
&lt;td&gt;Native chart data or a deliberate redraw&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;“Editable” is a spectrum, not a single checkbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. What appears when you move an object away?
&lt;/h2&gt;

&lt;p&gt;This is the test that polished before/after pictures often skip.&lt;/p&gt;

&lt;p&gt;Move the title to an empty corner. Is there a clean background underneath, or is the old title still visible? Move a card. Does it leave a rectangular hole or a blurry patch?&lt;/p&gt;

&lt;p&gt;Reconstructing the background behind a foreground element requires an educated guess: the original pixels were covered. On a plain gradient that may be easy. On a photograph or intricate texture, expect to inspect and possibly repair the fill. Keep the original image beside the reconstructed slide so you can spot changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Did the small details keep their meaning?
&lt;/h2&gt;

&lt;p&gt;A slide can look convincing at a glance while getting a tiny but important detail wrong: a percentage, axis label, date, unit, legend entry, or footnote. Compare each one against the source image. Pay extra attention to low-resolution text and labels placed over busy imagery.&lt;/p&gt;

&lt;p&gt;For data-heavy slides, I treat the converted file as a &lt;strong&gt;draft&lt;/strong&gt; until the numbers have been checked by a human. Visual similarity is not the same as factual accuracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Does it survive a round trip?
&lt;/h2&gt;

&lt;p&gt;Download the PPTX, close the web editor, and open the file in the PowerPoint application your team actually uses. Make one text edit and one layout edit &lt;em&gt;there&lt;/em&gt;. Check for substituted fonts, shifted objects, or a slide that became flat during export.&lt;/p&gt;

&lt;p&gt;This matters because an interactive browser preview can demonstrate the intended structure without proving that the downloaded file preserves it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why convert an image to editable PowerPoint?
&lt;/h2&gt;

&lt;p&gt;Imagine you inherit this “Genetic Discovery” slide as a single image and need to update its explanation for a new audience. Rebuilding the two-column layout, charts, and labels by hand would take time before you could even start editing the message. In the original image, none of those elements can be selected independently.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft7p8es5283ara1pjswwl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft7p8es5283ara1pjswwl.png" alt="Original flattened Genetic Discovery slide with two purple panels, text, and charts" width="800" height="460"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Before: a flattened slide image.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The next screenshot shows the slide in EditableImage’s Image to PPT editor. A text region in the right-hand panel is selected as a separate object. The visual layout is still recognizable, but there is now a starting point for changing copy and rearranging parts of the slide instead of recreating everything from a blank canvas.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft58qsrsc43yoqirm0xa7.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ft58qsrsc43yoqirm0xa7.png" alt="EditableImage browser editor showing the Genetic Discovery slide with a text object selected" width="800" height="554"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;After: a text object selected in the browser editor. This is not a screenshot of the exported PPTX opened in PowerPoint.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That is the practical value of an image-to-editable-PowerPoint workflow: it can turn a screenshot-only slide into a reconstruction draft you can work from. For an old presentation that needs a revised heading, a corrected paragraph, or a new arrangement, the time saved comes from &lt;strong&gt;not having to redraw the whole slide first&lt;/strong&gt;. The original remains beside the result, making it easier to spot what needs fixing.&lt;/p&gt;

&lt;p&gt;The trade-off is visible too: some dense text in this result is garbled or overlaps nearby content. Check every scientific label and number against the source before reuse. These screenshots show object separation &lt;strong&gt;in the browser editor&lt;/strong&gt;; they do not prove that chart values are native PowerPoint data, that every background is clean, or that the exported PPTX passes the round-trip test.&lt;/p&gt;

&lt;p&gt;If you have a slide you actually need to revise, &lt;a href="https://editableimage.com/image-to-ppt" rel="noopener noreferrer"&gt;try converting one image with EditableImage&lt;/a&gt;. Correct the obvious text errors, export the PPTX, and run the five tests above. Judge it by the manual work it saves on &lt;em&gt;your&lt;/em&gt; slide—not by a perfect-looking preview.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short test with your own slide
&lt;/h2&gt;

&lt;p&gt;If you have a screenshot-only slide, use a full-resolution PNG, JPG, or WebP rather than a compressed chat thumbnail. Then:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write down two edits you actually need to make—for example, change a KPI and move a card.&lt;/li&gt;
&lt;li&gt;Convert that one slide and compare the result with the original. Fix obvious recognition errors before export.&lt;/li&gt;
&lt;li&gt;Download the PPTX and run tests 1–5 in PowerPoint. Keep the source image next to it while you inspect small details.&lt;/li&gt;
&lt;li&gt;Judge the result by &lt;strong&gt;manual work avoided&lt;/strong&gt;, not by the number of objects reported. If your requirement is editable chart data or an exact licensed typeface, expect to rebuild or supply those parts yourself.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For context, the current EditableImage workflow requires sign-in, uses 4 credits per slide conversion, and gives new accounts 10 free credits without a card. The page estimates 45–90 seconds for processing. Images are sent to third-party AI services, are not published, and are deleted after 24 hours. Check the current terms before uploading anything confidential.&lt;/p&gt;

&lt;p&gt;If all you need is a slide that &lt;em&gt;looks&lt;/em&gt; right, simply placing the screenshot on a slide may be enough. If you need to reuse its words and layout, the exported file—not its extension or its preview—is the evidence.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I built EditableImage. AI assistance was used to draft and edit this article; the product details above were checked against its public pages. I have not presented the browser preview as an independent test of every exported PPTX.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>powerpoint</category>
      <category>design</category>
      <category>productivity</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I Recovered an Architecture Diagram from a PNG—Every Box Came Back, but the Labels Lied</title>
      <dc:creator>river</dc:creator>
      <pubDate>Sun, 27 Sep 2026 15:15:52 +0000</pubDate>
      <link>https://dev.to/river_jiang_670cccce7d97d/i-recovered-an-architecture-diagram-from-a-png-every-box-came-back-but-the-labels-lied-3h7c</link>
      <guid>https://dev.to/river_jiang_670cccce7d97d/i-recovered-an-architecture-diagram-from-a-png-every-box-came-back-but-the-labels-lied-3h7c</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4oedioy2ehkhamb0k5ce.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4oedioy2ehkhamb0k5ce.webp" alt="A checkout-flow architecture diagram — Browser, CDN, Load balancer, API gateway, Auth, Orders, Search, Payments and three data stores drawn as cylinders — as a flat image" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When the only copy of an architecture diagram is an image, you can recover it as an&lt;br&gt;
editable Draw.io file instead of redrawing it: the boxes and their labels come back as&lt;br&gt;
real shapes you can move, and the arrows come back as connectors. On the real example&lt;br&gt;
below, every box and every arrow came back — but the small words written along the arrows&lt;br&gt;
did not, and a few of them came back wrong. It is a draft to check, and this post is about&lt;br&gt;
what exactly to check.&lt;/p&gt;

&lt;h2&gt;
  
  
  How does a diagram end up as only a picture?
&lt;/h2&gt;

&lt;p&gt;The usual ways. A design session ends and someone photographs the whiteboard. A system is&lt;br&gt;
explained once in a slide deck, and the deck survives while the source file does not. A&lt;br&gt;
PNG gets pasted into the wiki by the person who then leaves. Six months later the diagram&lt;br&gt;
is needed for a review, it gets redrawn from memory, and the redraw gets a detail wrong&lt;br&gt;
that nobody can check.&lt;/p&gt;

&lt;p&gt;A picture cannot be searched, diffed against next quarter's design, or edited when the&lt;br&gt;
system changes. A diagram file can.&lt;/p&gt;

&lt;h2&gt;
  
  
  What does "recovered" mean?
&lt;/h2&gt;

&lt;p&gt;Not a trace. &lt;a href="https://editableimage.com/image-to-drawio" rel="noopener noreferrer"&gt;Image to Draw.io&lt;/a&gt; measures the boxes from the pixels, reads&lt;br&gt;
the labels, follows the arrow strokes, and attaches each connector to the shapes it joins.&lt;br&gt;
The result behaves like a diagram: drag a node and its edges go with it, retype a service&lt;br&gt;
name in place, export &lt;a href="https://www.diagrams.net/" rel="noopener noreferrer"&gt;Draw.io XML&lt;/a&gt; that opens anywhere&lt;br&gt;
&lt;a href="https://github.com/jgraph/drawio" rel="noopener noreferrer"&gt;mxGraph files&lt;/a&gt; are read.&lt;/p&gt;

&lt;p&gt;The diagram at the top of this post went in as a flat image. This is what came back:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzmaov3yovamhbwbg0l2a.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzmaov3yovamhbwbg0l2a.webp" alt="The checkout-flow diagram recovered as editable shapes in the Draw.io canvas, with a panel counting 13 nodes, 12 connectors and 13 labels" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The recovered diagram in the editor. The panel on the right counts what came back: 13 nodes, 12 connectors, 13 labels.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What did it get right, and what did it miss?
&lt;/h2&gt;

&lt;p&gt;Most of it. Taken from the recovered file itself — which boxes each connector is actually&lt;br&gt;
attached to, not how it happens to be drawn:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Every box came back&lt;/strong&gt;, in its place, with its label and its fill colour: the eleven
services and stores, plus the version tag and the sticky note. The three data stores
came back as cylinders.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All twelve connectors are joined to the right two boxes.&lt;/strong&gt; The nine arrows point the
right way, and the three plain lines down to the data stores stayed plain lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The dashed "production vpc" boundary is gone&lt;/strong&gt;, and its label with it. The boxes it
grouped are all there, just no longer inside anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The words on the lines are the weak part.&lt;/strong&gt; The original has six. One came back right
(&lt;em&gt;grpc&lt;/em&gt;), three came back missing (&lt;em&gt;https&lt;/em&gt;, &lt;em&gt;rest&lt;/em&gt;, &lt;em&gt;sql&lt;/em&gt;), and two came back as words the
original does not have: &lt;em&gt;get / set&lt;/em&gt; became &lt;em&gt;redis&lt;/em&gt;, and &lt;em&gt;events&lt;/em&gt; became &lt;em&gt;kafka&lt;/em&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Those last two are the ones worth dwelling on. They are plausible — a cache next to the&lt;br&gt;
word redis, a message queue next to kafka — which is exactly why they are easy to miss. A&lt;br&gt;
missing label is obvious; a wrong one that sounds right is not. The smaller slips are of&lt;br&gt;
the same kind: the tag's "·" came back as ":", and some rounded corners came back square.&lt;/p&gt;

&lt;p&gt;So the check that matters is not "does it look like the original", which it largely does.&lt;br&gt;
It is: read every word on a line against the original, then drag each box and watch which&lt;br&gt;
lines come with it. The original stays one click away in the editor for exactly this&lt;br&gt;
comparison, and each fix is a retype or a drag rather than a redraw.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is a whiteboard photo harder?
&lt;/h2&gt;

&lt;p&gt;Yes. The example above is a clean export, which is the easy input — and it still needed its&lt;br&gt;
line labels checked, which is worth saying plainly, because a clean export is the one every&lt;br&gt;
tool in this category shows. A photographed board adds a&lt;br&gt;
perspective the lens was not square to, a light reflection across some of the writing, and&lt;br&gt;
hand-drawn boxes without the consistent borders box detection leans on.&lt;/p&gt;

&lt;p&gt;Clear structure — boxes with borders, arrows with arrowheads, one label per edge — gives&lt;br&gt;
the best start. Dense freehand sketching comes back as a rougher draft with more arrows to&lt;br&gt;
fix. Either way the boxes and labels are the part you do not have to retype, and ten&lt;br&gt;
minutes of dragging connectors beats redrawing the whole thing from memory.&lt;br&gt;
&lt;a href="https://editableimage.com/whiteboard-photo-to-diagram" rel="noopener noreferrer"&gt;Whiteboard photo to diagram&lt;/a&gt; covers how to photograph a&lt;br&gt;
board so it can be read.&lt;/p&gt;

&lt;h2&gt;
  
  
  The steps
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://editableimage.com/image-to-drawio" rel="noopener noreferrer"&gt;Image to Draw.io&lt;/a&gt; and upload the picture.&lt;/li&gt;
&lt;li&gt;Read every label on a line against the original, then drag each box and watch which
lines come with it: is every arrow there, joined to the right two boxes, pointing the
right way?&lt;/li&gt;
&lt;li&gt;Fix what is wrong by reconnecting, dragging and retyping.&lt;/li&gt;
&lt;li&gt;Export the XML and put it where the next person will find it — versioned, searchable,
and checkable against the next redesign.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  What does it produce?
&lt;/h3&gt;

&lt;p&gt;Standard Draw.io XML — nodes, connectors and labels that open in diagrams.net and any tool that reads mxGraph files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Will the connectors follow the nodes?
&lt;/h3&gt;

&lt;p&gt;They come back as edges attached to shapes, so dragging a node carries its arrows with it — which is also the quickest way to find an arrow attached to the wrong shape. Check every edge is there, joins the right two boxes, and points the right way.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does it work on photographed whiteboards?
&lt;/h3&gt;

&lt;p&gt;When boxes and arrows are clear, yes. A photo is a harder input than a clean export, so expect more to correct — the whiteboard page covers how to photograph a board so it can be read.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>systemdesign</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
    <item>
      <title>Archify Online: Interactive Diagrams in Your Browser, No Agent Needed</title>
      <dc:creator>river</dc:creator>
      <pubDate>Mon, 21 Sep 2026 10:10:00 +0000</pubDate>
      <link>https://dev.to/river_jiang_670cccce7d97d/archify-online-interactive-diagrams-in-your-browser-no-agent-needed-44nd</link>
      <guid>https://dev.to/river_jiang_670cccce7d97d/archify-online-interactive-diagrams-in-your-browser-no-agent-needed-44nd</guid>
      <description>&lt;p&gt;You see the screenshots everywhere: architecture maps that light up a request path on click, fade everything else back, and let you walk a system layer by layer. They come from Archify — the open-source diagram project on GitHub (tt-a1i/archify), MIT-licensed — which has collected more than 65,000 stars since April 2026 (as of September 2026). Then you open the repository and hit the catch: Archify is not a website. It is an agent skill and a CLI, meant to live inside Claude Code, Codex, or a similar tool.&lt;/p&gt;

&lt;p&gt;Most people searching for "Archify online" want the maps, not the tooling around them. This post covers what Archify actually is, whether an official online version exists (short answer: no — by explicit design), and the browser port of the same engine that we run at mermaidonline.org — including what carries over, what had to change, and an honest comparison so you can pick the right form for your workflow.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2ti8tcotkj4kha3cn5z.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fb2ti8tcotkj4kha3cn5z.webp" alt="An interactive architecture map rendered in the browser: a route probe panel traces Order Service to RabbitMQ to Inventory Service to Postgres while unrelated nodes fade back." width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;TL;DR&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Archify is a 65,000+-star open-source engine that turns a system description into a verifiable, interactive map — but it ships as an agent skill/CLI, not a website.&lt;/li&gt;
&lt;li&gt;There is no official hosted version; the README names hosted sharing as intentionally out of scope.&lt;/li&gt;
&lt;li&gt;We ported the same MIT-licensed rendering engine to the browser: paste a description, a Mermaid flowchart, docker-compose, Terraform, Kubernetes, or SQL, and explore the result at the interactive map generator.&lt;/li&gt;
&lt;li&gt;Export is a self-contained HTML file — search, route tracing, and lenses keep working offline, in one file you can attach to a PR.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What Is Archify and Why Is It Everywhere?
&lt;/h2&gt;

&lt;p&gt;Archify is best understood as a compile step for system diagrams. Instead of asking a model to "draw" boxes and hoping the output is legible, an agent produces a typed JSON intermediate representation of your system, and Archify deterministically compiles that specification into self-contained HTML/SVG. The project's own README puts it simply: agents produce the typed specification; Archify deterministically renders it (project README on GitHub, accessed September 2026). That split is why the output looks designed rather than generated — layout, routing, and label spacing are rules, not guesses.&lt;/p&gt;

&lt;p&gt;It landed at the right moment. AI coding agents became the default place where engineers already describe systems, so "turn a codebase or system description into a polished, interactive system map — directly in chat" (project README) matched how people actually work. Growth has been steep: the repository was created in April 2026, crossed roughly 22,800 stars by late August, and sits above 65,000 as of September 2026 (GitHub repository and public star trackers).&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Project&lt;/td&gt;
&lt;td&gt;tt-a1i/archify on GitHub&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;License&lt;/td&gt;
&lt;td&gt;MIT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Form&lt;/td&gt;
&lt;td&gt;Agent skill + CLI (runs inside coding agents)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diagram types&lt;/td&gt;
&lt;td&gt;Architecture, workflow, sequence, data flow, lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Created&lt;/td&gt;
&lt;td&gt;April 2026 — 65,000+ stars as of September 2026&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The mental model to keep: &lt;strong&gt;Archify is a skill you install into your agent, not a site you visit.&lt;/strong&gt; Which raises the obvious question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is There an Official Archify Online Version?
&lt;/h2&gt;

&lt;p&gt;No — and this is a deliberate scope decision, not an oversight. The README states it verbatim:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Automatic Mermaid parsing, general-purpose auto-layout, hosted sharing, and WYSIWYG editing are intentionally outside the current scope." (project README on GitHub, accessed September 2026)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The official site is a documentation landing page plus a gallery of pre-built example artifacts you can download and open locally. Nothing on it accepts your input and returns a map. So today there are exactly two ways to run this engine:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;How it works&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Original (official)&lt;/td&gt;
&lt;td&gt;Install the skill into an agent (Claude Code, Codex, Cursor, …), describe your system in chat, receive the rendered map locally&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Browser port (ours)&lt;/td&gt;
&lt;td&gt;Open a web page, paste code or describe the system, explore the map immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Everything below is about the second row — what a browser port of the engine gives you, and where it deliberately differs.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Browser Port of the Same Engine: No Install, No Agent
&lt;/h2&gt;

&lt;p&gt;We run the ported engine at &lt;a href="https://mermaidonline.org/ai-architecture-diagram-generator" rel="noopener noreferrer"&gt;the interactive map generator&lt;/a&gt;. One disclosure first: Mermaid Online is not affiliated with or endorsed by the Archify authors — we ported its MIT-licensed engine and credit it by name in the product.&lt;/p&gt;

&lt;h3&gt;
  
  
  What carries over from the original
&lt;/h3&gt;

&lt;p&gt;The part of Archify that makes the output good is the deterministic layer, and that is exactly what the port preserves. Same typed JSON specification contract, same rule-driven layout and label-fitting, same interaction model: click a node to focus it, press &lt;code&gt;R&lt;/code&gt; to trace an exact route between two nodes, &lt;code&gt;L&lt;/code&gt; to compare semantic roles through lenses, &lt;code&gt;P&lt;/code&gt; to play a guided story, search with &lt;code&gt;/&lt;/code&gt;. Deep links carry state — a URL fragment like &lt;code&gt;#route=a~b&lt;/code&gt; reopens the map with that path already traced. Because the compilation is deterministic, the same specification renders identically in the CLI and in the browser; route traces over the same nodes return the same hops. The exported artifact is the same self-contained HTML file: one file, no CDN, no server, and the interactions keep working offline.&lt;/p&gt;

&lt;h3&gt;
  
  
  What we rebuilt for the browser
&lt;/h3&gt;

&lt;p&gt;The upstream flow assumes an agent sits in front of the engine: &lt;em&gt;you&lt;/em&gt; describe the system in chat, the agent authors the JSON specification. In a browser there is no agent, so our service takes over that step — an AI layer converts your input into the typed specification, and the renderer takes it from there. That substitution drives most of the practical differences below, including the inputs we accept (the original explicitly excludes automatic Mermaid parsing, while accepting a pasted Mermaid diagram is one of our primary paths). It also changes the economics: running the conversion costs us model calls, so the port is free for your first 10 maps and uses credits after that, while the CLI remains fully free under its MIT license.&lt;/p&gt;

&lt;h2&gt;
  
  
  From Mermaid, docker-compose, Terraform, K8s, or SQL to an Interactive Map
&lt;/h2&gt;

&lt;p&gt;The input story is where the port diverges most from upstream. The original expects your agent to read a repository or a chat description; Mermaid parsing is explicitly out of its scope. Our conversion layer was built for exactly the artifacts engineers already have lying around:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Input&lt;/th&gt;
&lt;th&gt;Original Archify&lt;/th&gt;
&lt;th&gt;Browser port&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Plain-English description&lt;/td&gt;
&lt;td&gt;✅ (in agent chat)&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mermaid flowchart&lt;/td&gt;
&lt;td&gt;❌ (out of scope by design)&lt;/td&gt;
&lt;td&gt;✅ paste it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;docker-compose / Terraform / K8s manifests&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ paste it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL schema&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ paste it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Paste a Mermaid flowchart
&lt;/h3&gt;

&lt;p&gt;This is the path most mermaidonline readers will care about. Take a flowchart you already maintain:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    A[Client] --&amp;gt; B[API Gateway]
    B --&amp;gt; C[Webhook Service]
    C --&amp;gt; D{Delivery ok?}
    D -- yes --&amp;gt; E[(Event Store)]
    D -- no --&amp;gt; F[Retry Queue]
    F --&amp;gt; C&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Paste it into &lt;a href="https://mermaidonline.org/ai-architecture-diagram-generator" rel="noopener noreferrer"&gt;the interactive map generator&lt;/a&gt;, and the conversion layer maps nodes to typed components, classifies them (external, backend, message bus, data store), and hands the specification to the same deterministic renderer:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2zg2n8qcpmggexmkefp2.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2zg2n8qcpmggexmkefp2.webp" alt="The Mermaid flowchart pasted into the generator, and the interactive Webhook Delivery System map it produced, with typed nodes and a legend" width="800" height="577"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The static diagram did not change — the same six nodes and edges are all there. What changed is that every box became an addressable object you can search, trace, and filter. If your Mermaid does not render cleanly to begin with, fix that first with &lt;a href="https://mermaidonline.org/blog/ai-mermaid-diagrams" rel="noopener noreferrer"&gt;our guide to getting AI-generated Mermaid to render correctly&lt;/a&gt;, or generate fresh code with &lt;a href="https://mermaidonline.org/ai-mermaid-generator" rel="noopener noreferrer"&gt;the AI Mermaid generator&lt;/a&gt;. Photos of whiteboards work too — &lt;a href="https://mermaidonline.org/image-to-mermaid" rel="noopener noreferrer"&gt;turn a whiteboard photo into Mermaid&lt;/a&gt; first, then paste the result. And if a static Mermaid architecture diagram is all you need, the &lt;a href="https://mermaidonline.org/mermaid-architecture-diagram" rel="noopener noreferrer"&gt;Mermaid architecture diagram guide&lt;/a&gt; covers that path.&lt;/p&gt;

&lt;h3&gt;
  
  
  Paste infrastructure as code
&lt;/h3&gt;

&lt;p&gt;The same conversion layer reads infrastructure definitions. A &lt;code&gt;docker-compose.yml&lt;/code&gt; becomes services and their dependencies; a Terraform file becomes managed resources and their references; Kubernetes manifests become workloads, config, and the clusters they expose. The value over reading the YAML is the same as with Mermaid: the map exposes &lt;em&gt;structure&lt;/em&gt; — what calls what, what happens if this node dies — which is tedious to reconstruct by eye from a few hundred lines of manifests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Point it at a SQL schema
&lt;/h3&gt;

&lt;p&gt;Foreign keys are edges; tables are nodes. A schema paste produces a map where you can trace exactly which tables a given feature touches — useful during migrations and reviews, when the question "what reads this table?" needs a precise answer. As with every input mode, the result is an explorable map, not a picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Map Can Do: Node Search, Route Tracing, Lenses, Guided Stories
&lt;/h2&gt;

&lt;p&gt;The interactions are the reason to bother with an engine like this at all. Four of them carry most of the value:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node search&lt;/strong&gt; (&lt;code&gt;/&lt;/code&gt;) jumps the viewport to any named node — the fast path in a map with dozens of components.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Route tracing&lt;/strong&gt; (&lt;code&gt;R&lt;/code&gt;) asks the only question that matters in an incident review: &lt;em&gt;how does a request actually get from A to B?&lt;/em&gt; The map highlights the exact directed hops and fades everything else.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqd6i9tw6q0jp8yruj53a.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqd6i9tw6q0jp8yruj53a.webp" alt="A data-flow map with route tracing active: the path between the first and last pipeline stages is highlighted while the rest of the map fades" width="800" height="433"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Semantic lenses&lt;/strong&gt; (&lt;code&gt;L&lt;/code&gt;) re-color the map by component role — backends, data stores, external services — so you can read one layer at a time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F67o4yjshsk18y4ubazym.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F67o4yjshsk18y4ubazym.webp" alt="The same map viewed through a semantic lens: backend and database components highlighted with the legend role chips selected" width="800" height="595"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Guided stories&lt;/strong&gt; (&lt;code&gt;P&lt;/code&gt;) play an authored walkthrough of the system, one step at a time — the closest thing to a presenter mode a generated map has.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two of these survive export in a shareable form. Deep links like &lt;code&gt;#route=&lt;/code&gt; and &lt;code&gt;#lens=&lt;/code&gt; reopen the exported HTML with the view already applied, which makes them surprisingly useful in PR descriptions and design docs — paste a link and reviewers see the path you are talking about, not a red rectangle drawn on a screenshot. For destinations that cannot run HTML at all, &lt;a href="https://mermaidonline.org/mermaid-to-png" rel="noopener noreferrer"&gt;export a static PNG when the destination can't run HTML&lt;/a&gt; instead; and if you are weighing interactive HTML against plain Mermaid blocks, &lt;a href="https://mermaidonline.org/mermaid-in-github" rel="noopener noreferrer"&gt;how Mermaid renders in GitHub README files&lt;/a&gt; covers the trade-offs.&lt;/p&gt;

&lt;h2&gt;
  
  
  60-Second Walkthrough: Mermaid Flowchart to Interactive Map
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://mermaidonline.org/ai-architecture-diagram-generator" rel="noopener noreferrer"&gt;the interactive map generator&lt;/a&gt; — no install, no signup to try.&lt;/li&gt;
&lt;li&gt;Paste a Mermaid flowchart, docker-compose, Terraform, Kubernetes, or SQL — or just describe the system in plain English. The input type is detected automatically.&lt;/li&gt;
&lt;li&gt;Generate. The map renders in the page; search (&lt;code&gt;/&lt;/code&gt;) for a node name to test that everything landed.&lt;/li&gt;
&lt;li&gt;Press &lt;code&gt;R&lt;/code&gt; and pick two nodes to trace a route; press &lt;code&gt;L&lt;/code&gt; to view the map through a role lens.&lt;/li&gt;
&lt;li&gt;Download the self-contained HTML — it opens offline with every interaction intact.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That is the whole loop. There is nothing to configure before step 2; the type chips (Architecture, Dataflow, Sequence) exist if you want to force a diagram style instead of relying on detection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest Comparison: Browser Port vs the Original Archify CLI
&lt;/h2&gt;

&lt;p&gt;If you are evaluating this family of tools, the comparison that matters is not "port versus original" as a quality ranking — the port uses the original's engine. It is which &lt;em&gt;form&lt;/em&gt; fits your workflow:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Original Archify (agent skill/CLI)&lt;/th&gt;
&lt;th&gt;Browser port (mermaidonline.org)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Price&lt;/td&gt;
&lt;td&gt;Free (MIT)&lt;/td&gt;
&lt;td&gt;10 free maps, then paid credits&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Install&lt;/td&gt;
&lt;td&gt;Agent + skill/CLI&lt;/td&gt;
&lt;td&gt;None — a browser tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Diagram types&lt;/td&gt;
&lt;td&gt;5 (incl. workflow, lifecycle)&lt;/td&gt;
&lt;td&gt;3 (architecture, dataflow, sequence)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inputs&lt;/td&gt;
&lt;td&gt;Whatever your agent can read (repos, chats)&lt;/td&gt;
&lt;td&gt;Description or pasted Mermaid / compose / Terraform / K8s / SQL&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Output&lt;/td&gt;
&lt;td&gt;Self-contained interactive HTML&lt;/td&gt;
&lt;td&gt;Same engine, same self-contained HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reproducibility&lt;/td&gt;
&lt;td&gt;Spec + deterministic compile&lt;/td&gt;
&lt;td&gt;Spec + deterministic compile&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  When the original CLI is the better choice
&lt;/h3&gt;

&lt;p&gt;If you already live in Claude Code or Codex, the skill is the more natural home: it can read your repository directly, it is free without limits, and it tracks upstream immediately. You also get the two diagram types the port does not serve yet — workflow and lifecycle — plus whatever the fast-moving upstream adds next (the project has shipped continuously since April).&lt;/p&gt;

&lt;h3&gt;
  
  
  Limitations of our port
&lt;/h3&gt;

&lt;p&gt;The port covers three of the five diagram types; upstream updates reach it only when we re-port them; the input conversion runs through our AI layer rather than your local agent; and after the first 10 free maps it is paid credits. None of that is hidden in the fine print — it is the trade you make for zero install. If those constraints bite, use the original; that is what it is for. And if the interactive layer is more than you need, a well-structured static diagram still goes a long way — &lt;a href="https://mermaidonline.org/large-mermaid-diagrams" rel="noopener noreferrer"&gt;keeping large diagrams readable&lt;/a&gt; applies regardless of the render.&lt;/p&gt;

&lt;h2&gt;
  
  
  Frequently Asked Questions
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Is Archify free to use?
&lt;/h3&gt;

&lt;p&gt;The upstream project is MIT-licensed open source, so the CLI and agent skill cost nothing. Our browser port is free to try with no account — you get 10 interactive renders, and paid credits cover heavier use.&lt;/p&gt;

&lt;h3&gt;
  
  
  Does Archify have an official online version?
&lt;/h3&gt;

&lt;p&gt;No. As of September 2026, the project README states that hosted sharing is "intentionally outside the current scope", and the official site hosts documentation plus a gallery of prebuilt examples. The only way to run the engine in a browser today is a third-party port like ours.&lt;/p&gt;

&lt;h3&gt;
  
  
  Can I use Archify without an AI agent or CLI?
&lt;/h3&gt;

&lt;p&gt;The original Archify is an agent skill and CLI designed to run inside tools like Claude Code. Our port moves the rendering engine into the browser, so you can paste Mermaid, docker-compose, Terraform, Kubernetes manifests, or a SQL schema and get an interactive map with nothing to install.&lt;/p&gt;

&lt;h3&gt;
  
  
  What export formats does Archify support?
&lt;/h3&gt;

&lt;p&gt;The core output is a self-contained interactive HTML file that works offline — node search, route tracing, and lenses keep functioning in the exported file. If you need a static image for slides or docs, you can export one with our Mermaid-to-PNG tool instead.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It While You Are Here
&lt;/h2&gt;

&lt;p&gt;Everything above runs at &lt;a href="https://mermaidonline.org/" rel="noopener noreferrer"&gt;mermaidonline.org&lt;/a&gt; — the &lt;a href="https://mermaidonline.org/ai-architecture-diagram-generator" rel="noopener noreferrer"&gt;interactive map generator&lt;/a&gt; needs no account for your first 10 maps, and the same site covers the static side of the workflow: generating Mermaid from a description or an image, checking syntax, and exporting PNG/SVG. If you end up preferring the original CLI, the repository README has the install instructions — the engine is the same either way; pick the form that matches where you work.&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>“Looks done” is not the same as launch-ready</title>
      <dc:creator>river</dc:creator>
      <pubDate>Wed, 17 Jun 2026 09:35:43 +0000</pubDate>
      <link>https://dev.to/river_jiang_670cccce7d97d/looks-done-is-not-the-same-as-launch-ready-d19</link>
      <guid>https://dev.to/river_jiang_670cccce7d97d/looks-done-is-not-the-same-as-launch-ready-d19</guid>
      <description>&lt;p&gt;I used to check small sites in a pretty shallow way before launch.&lt;/p&gt;

&lt;p&gt;Open the homepage.&lt;br&gt;&lt;br&gt;
Click the main CTA.&lt;br&gt;&lt;br&gt;
Resize the browser.&lt;br&gt;&lt;br&gt;
Make sure nothing looks obviously broken.&lt;/p&gt;

&lt;p&gt;If the page looked fine, I shipped it.&lt;/p&gt;

&lt;p&gt;But the issues I kept missing were rarely visual.&lt;/p&gt;

&lt;p&gt;A staging noindex tag was still there.&lt;br&gt;&lt;br&gt;
The canonical URL pointed to a preview domain.&lt;br&gt;&lt;br&gt;
A privacy or contact link returned 404.&lt;br&gt;&lt;br&gt;
The sitemap included old test pages.&lt;br&gt;&lt;br&gt;
The shared link preview looked broken.&lt;br&gt;&lt;br&gt;
The homepage copy was clear to me, but not clear enough for someone seeing it cold.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbqzc8jufxy1h1kywy0s6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbqzc8jufxy1h1kywy0s6.jpg" alt=" " width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;None of these are dramatic bugs.&lt;/p&gt;

&lt;p&gt;But they matter because launch traffic usually comes in a short window. You post once on Product Hunt, Reddit, HN, a newsletter, or send the link to users. That first impression is shaped by more than the hero section.&lt;/p&gt;

&lt;p&gt;So my pre-launch check changed.&lt;/p&gt;

&lt;p&gt;Now I try to answer a few boring but useful questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can search engines index the right pages?&lt;/li&gt;
&lt;li&gt;Are canonical, sitemap, and robots signals consistent?&lt;/li&gt;
&lt;li&gt;Do trust links like privacy, terms, and contact actually work?&lt;/li&gt;
&lt;li&gt;Does the page explain what the product is and who it’s for?&lt;/li&gt;
&lt;li&gt;Does the link preview look reasonable when shared?&lt;/li&gt;
&lt;li&gt;Are there any leftover staging or test signals?&lt;/li&gt;
&lt;li&gt;For AI/devtool sites: is there enough public context for an assistant to summarize it correctly?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fykf1umkudcn1vpvqkzdo.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fykf1umkudcn1vpvqkzdo.jpg" alt=" " width="799" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That last one is becoming more important for me.&lt;/p&gt;

&lt;p&gt;I don’t think anyone can honestly promise “AI visibility.” But many sites accidentally make themselves harder to understand: thin copy, missing docs, blocked crawlers, placeholder llms.txt, unclear positioning.&lt;/p&gt;

&lt;p&gt;Before sharing a site now, I try to ask:&lt;/p&gt;

&lt;p&gt;If someone — or some crawler — sees this page without context, will they understand it correctly?&lt;/p&gt;

&lt;p&gt;I turned this checklist into a small free tool here:&lt;br&gt;&lt;br&gt;
&lt;a href="https://websiteready.org/?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=launch_ready_checklist" rel="noopener noreferrer"&gt;WebsiteReady launch checker&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;But the habit is the useful part:&lt;/p&gt;

&lt;p&gt;Don’t only check whether the site looks finished.&lt;/p&gt;

&lt;p&gt;Check whether it is ready to be discovered, shared, indexed, and trusted.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>productivity</category>
      <category>ai</category>
      <category>security</category>
    </item>
    <item>
      <title>I wanted cleaner beautiful Mermaid diagrams for docs, so I made a small exporter</title>
      <dc:creator>river</dc:creator>
      <pubDate>Thu, 04 Jun 2026 07:56:46 +0000</pubDate>
      <link>https://dev.to/river_jiang_670cccce7d97d/i-wanted-cleaner-beautiful-mermaid-diagrams-for-docs-so-i-made-a-small-exporter-3b6</link>
      <guid>https://dev.to/river_jiang_670cccce7d97d/i-wanted-cleaner-beautiful-mermaid-diagrams-for-docs-so-i-made-a-small-exporter-3b6</guid>
      <description>&lt;p&gt;I use Mermaid diagrams a lot when I am trying to explain something that is easier to see than to read.&lt;/p&gt;

&lt;p&gt;A release flow. A service talking to another service. A small state machine. The kind of diagram that belongs in a README, an architecture note, or a planning doc.&lt;/p&gt;

&lt;p&gt;Mermaid itself is great for that. Plain text, versionable, reviewable. The part that kept annoying me was what happened after the diagram looked right.&lt;/p&gt;

&lt;p&gt;I often wanted a clean image for a README, a slide, or a blog post. Not a rough screenshot. Not something with awkward padding. Not a diagram that looked fine in one place and slightly blurry everywhere else.&lt;/p&gt;

&lt;p&gt;So I made a small tool for that:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mermaidonline.org/?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=launch_mermaidonline_202606&amp;amp;utm_content=showdev_docs" rel="noopener noreferrer"&gt;Mermaid Online&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgcfuoryazfojs0ha7gtd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgcfuoryazfojs0ha7gtd.png" alt=" " width="800" height="492"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It is a browser-based Mermaid editor and exporter. Paste Mermaid code, preview it, adjust the export style, then download PNG, SVG, JPG, WebP, or PDF.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw30o4uwfurogncc56ykp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fw30o4uwfurogncc56ykp.png" alt=" " width="800" height="475"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;No signup. No watermark. The core render/export workflow happens in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I wanted to smooth out
&lt;/h2&gt;

&lt;p&gt;Here is a tiny example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart LR
  A[Write the doc] --&amp;gt; B[Sketch the flow]
  B --&amp;gt; C[Preview the diagram]
  C --&amp;gt; D[Export for README]
  C --&amp;gt; E[Export for slides]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is easy enough to write.&lt;/p&gt;

&lt;p&gt;But when I need to put it somewhere, I usually start making small judgement calls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Should this be SVG or PNG?&lt;/li&gt;
&lt;li&gt;Does it need a transparent background?&lt;/li&gt;
&lt;li&gt;Is 1x too soft for a slide?&lt;/li&gt;
&lt;li&gt;How much padding should the diagram have?&lt;/li&gt;
&lt;li&gt;Will it still be readable in a GitHub README?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are hard problems. That is partly why they are annoying. They are small enough that I do not want to open a design tool, but visible enough that a sloppy export makes the whole doc feel less cared for.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuf3oo9r7ym13hkrbv78p.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fuf3oo9r7ym13hkrbv78p.png" alt=" " width="800" height="481"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What Mermaid Online does
&lt;/h2&gt;

&lt;p&gt;The tool is intentionally simple.&lt;/p&gt;

&lt;p&gt;You paste or edit Mermaid code on the left, check the live preview, then choose the export format and style.&lt;/p&gt;

&lt;p&gt;The formats I cared about most:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PNG for README files, docs, and slides&lt;/li&gt;
&lt;li&gt;SVG when I want a vector asset&lt;/li&gt;
&lt;li&gt;PDF for handoff or document workflows&lt;/li&gt;
&lt;li&gt;WebP for lightweight blog images&lt;/li&gt;
&lt;li&gt;JPG when I need the boring compatible option&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There are also presets for common destinations: clean docs, GitHub README, transparent backgrounds, dark docs, slides, and blog-style cards.&lt;/p&gt;

&lt;p&gt;That last bit matters more than I expected. A diagram for a README and a diagram for a presentation usually should not have exactly the same padding, background, and scale.&lt;/p&gt;

&lt;h2&gt;
  
  
  A workflow that feels right to me
&lt;/h2&gt;

&lt;p&gt;For documentation, I usually do this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write the Mermaid diagram directly while working on the doc.&lt;/li&gt;
&lt;li&gt;Paste it into Mermaid Online.&lt;/li&gt;
&lt;li&gt;Use a docs or README preset.&lt;/li&gt;
&lt;li&gt;Export PNG at 2x, or SVG if the destination handles it well.&lt;/li&gt;
&lt;li&gt;Put both the Mermaid source and exported asset near the doc.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For slides, I usually switch to a transparent preset and export at a larger scale. That keeps labels and arrows sharper on big screens, especially when the diagram gets resized by the slide tool.&lt;/p&gt;

&lt;p&gt;For blog posts, WebP is nice when I want something lighter, and the editorial-style preset gives the diagram enough breathing room that it does not look like a raw screenshot dropped into the middle of the page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why local-first mattered
&lt;/h2&gt;

&lt;p&gt;Some Mermaid diagrams are harmless. Others describe internal systems, database relationships, release flows, or security-review details.&lt;/p&gt;

&lt;p&gt;I did not want the default workflow to require uploading diagram text just to turn it into an image.&lt;/p&gt;

&lt;p&gt;The core editor and exporter run in the browser, so the diagram content stays local during the normal export path. That makes the tool feel more appropriate for architecture notes and internal docs.&lt;/p&gt;

&lt;p&gt;It is still a small project, so I am being careful with the wording here: this is an independent tool, not affiliated with Mermaid.js, Mermaid Chart, or their maintainers.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would like to improve next
&lt;/h2&gt;

&lt;p&gt;The current version is mostly about single-diagram export quality.&lt;/p&gt;

&lt;p&gt;The things I keep thinking about next are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Batch export from Markdown files&lt;/li&gt;
&lt;li&gt;ZIP downloads for multiple diagrams&lt;/li&gt;
&lt;li&gt;Saved custom themes&lt;/li&gt;
&lt;li&gt;Better workflows for docs teams&lt;/li&gt;
&lt;li&gt;More examples for common architecture and product diagrams&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I am deliberately not rushing all of that in at once. The first version should do one job well: turn Mermaid code into a clean asset you can actually use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;If you use Mermaid in README files, technical docs, slides, or blog posts, you can try it here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://mermaidonline.org/?utm_source=devto&amp;amp;utm_medium=community&amp;amp;utm_campaign=launch_mermaidonline_202606&amp;amp;utm_content=showdev_docs" rel="noopener noreferrer"&gt;https://mermaidonline.org/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I would especially like feedback from people who write docs often:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which export format do you use most?&lt;/li&gt;
&lt;li&gt;Do you prefer keeping Mermaid source in the doc, or committing generated images?&lt;/li&gt;
&lt;li&gt;What diagram style actually looks good inside your documentation system?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Small tools get better when they are shaped by real workflows, so practical feedback is more useful to me than general praise.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>ai</category>
      <category>mermaid</category>
    </item>
  </channel>
</rss>
