<?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: Aihangsoft Tools</title>
    <description>The latest articles on DEV Community by Aihangsoft Tools (@aihangsoft).</description>
    <link>https://dev.to/aihangsoft</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%2F4144253%2F97a67330-ba0f-4f8d-ab89-de7fad56dfc4.png</url>
      <title>DEV Community: Aihangsoft Tools</title>
      <link>https://dev.to/aihangsoft</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aihangsoft"/>
    <language>en</language>
    <item>
      <title>Trimming Video in the Browser: Why "Precise" and "Fast" Give Different Cuts</title>
      <dc:creator>Aihangsoft Tools</dc:creator>
      <pubDate>Mon, 28 Sep 2026 01:43:03 +0000</pubDate>
      <link>https://dev.to/aihangsoft/trimming-video-in-the-browser-why-precise-and-fast-give-different-cuts-4cfo</link>
      <guid>https://dev.to/aihangsoft/trimming-video-in-the-browser-why-precise-and-fast-give-different-cuts-4cfo</guid>
      <description>&lt;p&gt;There are two ways to cut a clip, and they answer different questions. A precise cut re-encodes the video, so the cut lands on exactly the frame you chose. A fast cut copies the compressed stream without decoding it, so the cut snaps to the nearest keyframe and the start can shift. Neither approach is wrong. Each is a trade between accuracy and speed.&lt;/p&gt;

&lt;p&gt;The rest of this post explains why, with the actual ffmpeg commands for both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a cut cannot always be arbitrary
&lt;/h2&gt;

&lt;p&gt;Video is not stored as a sequence of independent frames. Most frames are described relative to other frames. A keyframe, also called an I-frame, is self-contained: it can be decoded on its own. The frames after it carry only the differences. The stretch from one keyframe to the next is a GOP, a group of pictures.&lt;/p&gt;

&lt;p&gt;This is why a stream copy cut cannot start anywhere. If you ask to start at 4.2 seconds and there is no keyframe at 4.2 seconds, the tool cannot decode from there, because the frames at 4.2 seconds are defined relative to an earlier keyframe. Copying the compressed bits from 4.2 seconds would produce a broken first frame.&lt;/p&gt;

&lt;p&gt;Encoders decide where keyframes go. A typical encoder places one every few seconds, plus at scene changes where a fresh reference makes sense. That spacing is usually not exposed to you, which is why a stream copy cut can land up to about one GOP away from where you asked. The longer the GOP, the larger the possible offset.&lt;/p&gt;

&lt;p&gt;Both modes exist because of this constraint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fast mode: stream copy
&lt;/h2&gt;

&lt;p&gt;Fast mode never decodes the video. It copies the compressed packets from one point to another:&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;-ss&lt;/span&gt; 00:00:04 &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-t&lt;/span&gt; 6 &lt;span class="nt"&gt;-c&lt;/span&gt; copy output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-ss 00:00:04&lt;/code&gt; is the start time.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-i input.mp4&lt;/code&gt; is the source.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-t 6&lt;/code&gt; keeps 6 seconds from that start.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-c copy&lt;/code&gt; is the key flag: copy every stream as is, with no re-encode.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because nothing is re-encoded, this is fast and lossless. The audio and video bytes are identical to the source, just a different slice of them.&lt;/p&gt;

&lt;p&gt;The catch is the start point. With &lt;code&gt;-ss&lt;/code&gt; placed before &lt;code&gt;-i&lt;/code&gt;, ffmpeg seeks to the nearest point it can actually begin at, which is a keyframe. If the nearest keyframe is at 3.9 seconds and you asked for 4.2 seconds, the output begins at 3.9 seconds. The cut can only ever land on keyframes.&lt;/p&gt;

&lt;p&gt;Placing &lt;code&gt;-ss&lt;/code&gt; before &lt;code&gt;-i&lt;/code&gt; also makes the seek fast, because ffmpeg jumps to the nearest keyframe instead of decoding from zero. Move &lt;code&gt;-ss&lt;/code&gt; after &lt;code&gt;-i&lt;/code&gt; and it decodes from the start of the file up to the requested point, which is slower. For a stream copy, the keyframe limit still applies either way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Precise mode: re-encode
&lt;/h2&gt;

&lt;p&gt;Precise mode decodes and re-encodes the selected range, so the cut lands on the frame you chose:&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;-ss&lt;/span&gt; 00:00:04.2 &lt;span class="nt"&gt;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-t&lt;/span&gt; 6 &lt;span class="nt"&gt;-c&lt;/span&gt;:v libx264 &lt;span class="nt"&gt;-crf&lt;/span&gt; 20 &lt;span class="nt"&gt;-preset&lt;/span&gt; veryfast &lt;span class="nt"&gt;-c&lt;/span&gt;:a aac &lt;span class="nt"&gt;-b&lt;/span&gt;:a 128k &lt;span class="nt"&gt;-movflags&lt;/span&gt; +faststart output.mp4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-c:v libx264&lt;/code&gt; re-encodes the video with H.264.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-crf 20&lt;/code&gt; is the quality target. CRF stands for Constant Rate Factor: lower means higher quality and a bigger file, higher means smaller and rougher. 20 is a visually good default.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-preset veryfast&lt;/code&gt; trades compression efficiency for speed. A slower preset squeezes a slightly smaller file at the same quality, at the cost of encoding time.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-c:a aac -b:a 128k&lt;/code&gt; re-encodes the audio to AAC at 128 kbps.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-movflags +faststart&lt;/code&gt; moves the MP4 index to the front of the file, so playback can start before the whole file has downloaded.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because this path decodes and rebuilds the frames, the start can be 4.2 seconds exactly. It also means the output is a new generation of encoding, and it takes far longer than a copy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one to use
&lt;/h2&gt;

&lt;p&gt;Use fast mode when the original must stay bit for bit identical, when speed matters, and when being off by a fraction of a second is acceptable. It is the right choice for a rough cut, or for a pre-trim before another step.&lt;/p&gt;

&lt;p&gt;Use precise mode when the start and end must land on exact frames, for example cutting on a beat, on a title card, or to a fixed length with no drift. If you are unsure, prefer the precise cut and accept the re-encode: fast mode is for speed, not for accuracy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Doing this in the browser
&lt;/h2&gt;

&lt;p&gt;Both modes are plain ffmpeg, so both run under ffmpeg.wasm. The engine is around 30 MB and is fetched on first use, then cached and works offline. Because the same engine runs the same command line, switching between modes is just a change of flags, with no second implementation that behaves differently.&lt;/p&gt;

&lt;p&gt;The video never leaves the device. There is no upload step, so a large source is fine on a slow connection, and nothing is left on a server afterwards. The output is always MP4, written with &lt;code&gt;+faststart&lt;/code&gt; so it can stream while it downloads. The shortest range you can keep is 0.2 seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this tool does not do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One continuous range.&lt;/strong&gt; It trims a single segment. To remove several pieces, you run it more than once.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cutting is not reversible.&lt;/strong&gt; Once the frames are dropped, they are gone from the output. Keep the source file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It does not change the resolution or the codec.&lt;/strong&gt; The output matches the input video. Re-encoding on the precise path keeps the same frame size and the same H.264 format; it does not resize the video or convert it to another codec.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It is not a compressor.&lt;/strong&gt; Precise mode re-encodes, but the goal is an accurate cut, not the smallest possible file.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Long videos are limited by memory.&lt;/strong&gt; A browser tab has a memory ceiling, so a very long or very high resolution source can fail where a desktop build of ffmpeg would not.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want a smaller file rather than a shorter clip, trim first and compress afterwards, or accept that a re-encode is the cost of an accurate cut.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;The full write-up, including the cases where keyframe misalignment actually bites, is at &lt;a href="https://www.aihangsoft.top/blog/trim-video-online.html" rel="noopener noreferrer"&gt;https://www.aihangsoft.top/blog/trim-video-online.html&lt;/a&gt;. The trimmer runs entirely in your browser and is free.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>webassembly</category>
    </item>
    <item>
      <title>Why Your Video-to-GIF Conversions Look Dirty, and the Two-Pass Palette Fix</title>
      <dc:creator>Aihangsoft Tools</dc:creator>
      <pubDate>Mon, 28 Sep 2026 01:40:37 +0000</pubDate>
      <link>https://dev.to/aihangsoft/why-your-video-to-gif-conversions-look-dirty-and-the-two-pass-palette-fix-3ii7</link>
      <guid>https://dev.to/aihangsoft/why-your-video-to-gif-conversions-look-dirty-and-the-two-pass-palette-fix-3ii7</guid>
      <description>&lt;p&gt;Most video-to-GIF conversions look dirty for one reason: they quantize every frame against a &lt;strong&gt;generic palette&lt;/strong&gt;. A fixed 256-color table has no idea that your clip is mostly dark green grass with a single red ball in it. The fix is a two-pass approach: build a palette that belongs to this one video, then map the video onto it.&lt;/p&gt;

&lt;p&gt;That is the whole idea. The rest of this post is the mechanics, plus an honest list of what a GIF still cannot do.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a generic palette fails
&lt;/h2&gt;

&lt;p&gt;A typical converter does one of two things. It maps every frame onto the web-safe 216-color table, which ignores the real colors of your clip entirely. Or it builds one palette from the first frame and reuses it for the whole clip, which only works if the color story never changes. One cut, one camera pan, or one light change, and the reused palette is wrong for everything after it.&lt;/p&gt;

&lt;p&gt;Either way, the 256 slots get spent on colors that are not in your video, and the colors that are in your video get approximated.&lt;/p&gt;

&lt;h2&gt;
  
  
  GIF gives you 256 colors, and that is a hard ceiling
&lt;/h2&gt;

&lt;p&gt;A GIF frame is not RGB. It is an indexed image: each pixel is an index into a color table, and that table holds at most 256 entries. Video is roughly 16.7 million possible RGB values. Converting to GIF is lossy quantization, and no filter chain can bring back what the index step discards.&lt;/p&gt;

&lt;p&gt;Dithering helps with the symptom. It spreads the quantization error spatially so gradients stop banding into flat steps. But dithering is a perceptual trick, it does not add colors. On noisy live-action footage it also adds visible grain and can make the file bigger, because a noisy pattern compresses worse.&lt;/p&gt;

&lt;p&gt;So the goal is not a clean image. The goal is a palette built from your actual frames, so the 256 slots land where the colors are.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two-pass filter chain
&lt;/h2&gt;

&lt;p&gt;Both passes live in a single filter_complex graph, which is why the split filter appears:&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;-i&lt;/span&gt; input.mp4 &lt;span class="nt"&gt;-filter_complex&lt;/span&gt; &lt;span class="s2"&gt;"[0:v] fps=15, scale=480:-2:flags=lanczos, split [a][b]; [a] palettegen=max_colors=256 [p]; [b][p] paletteuse=dither=sierra2_4a:diff_mode=rectangle"&lt;/span&gt; &lt;span class="nt"&gt;-loop&lt;/span&gt; 0 output.gif
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walking through the pieces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;fps=15&lt;/code&gt; reduces the source to 15 frames per second. GIF has no inter-frame compression worth depending on, so every kept frame is a full indexed image. Fewer frames means a smaller file. 15 is a common balance point for screen recordings and short UI clips.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;scale=480:-2:flags=lanczos&lt;/code&gt; resizes to 480 px wide and computes the height to preserve the aspect ratio, forced to an even number by &lt;code&gt;-2&lt;/code&gt;. &lt;code&gt;lanczos&lt;/code&gt; resamples more sharply than the default bilinear filter, which matters because a soft downscale hides detail the color step is about to mangle anyway.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;split [a][b]&lt;/code&gt; duplicates the processed stream. This is the part people skip and then wonder why nothing works. &lt;code&gt;palettegen&lt;/code&gt; has to look at the video to build a palette, and &lt;code&gt;paletteuse&lt;/code&gt; has to apply that palette to the same frames. Without &lt;code&gt;split&lt;/code&gt;, one branch would consume the stream and the other would have nothing, or you would build a palette from a different input than the one you convert. The split feeds both branches from a single decode.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;palettegen=max_colors=256&lt;/code&gt; emits one PNG holding the 256 colors chosen across the sampled frames.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;paletteuse=dither=sierra2_4a:diff_mode=rectangle&lt;/code&gt; applies the palette. &lt;code&gt;sierra2_4a&lt;/code&gt; is an error-diffusion dither that looks cleaner than classic Floyd-Steinberg on video. &lt;code&gt;diff_mode=rectangle&lt;/code&gt; restricts the error diffusion to the rectangle that actually changed between two frames, which speeds the encode up and keeps static regions from shimmering.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-loop 0&lt;/code&gt; writes the Netscape looping extension with an infinite loop count. Without it, the GIF plays once and stops.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can also run this as two separate ffmpeg calls, first to a palette PNG, then to the GIF. The single graph just avoids an intermediate file and a second decode.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three parameters decide the file size
&lt;/h2&gt;

&lt;p&gt;With the pipeline fixed, size is controlled by three knobs, and they multiply:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Duration.&lt;/strong&gt; GIF size is close to linear in the number of frames kept. 2 to 5 seconds is the practical range. Past that, the file grows faster than most people will tolerate. If you need a long clip, GIF is the wrong format.&lt;br&gt;
&lt;strong&gt;Frame rate.&lt;/strong&gt; 10, 15, 20 and 24 fps are the useful choices. 10 is visibly choppy on motion, 15 is fine for most UI and screen recordings, and 20 or 24 smooth things out at a size cost roughly proportional to the extra frames.&lt;br&gt;
&lt;strong&gt;Width.&lt;/strong&gt; 320, 480, 640 px, or the original width. Pixel count scales with the square of the width, so doubling the width quadruples the pixels, and the file grows in the same ballpark.&lt;/p&gt;

&lt;p&gt;There is no quality dial on top of these. The format has LZW compression, but that is lossless over the indexed data, it only removes redundancy. It does not let you trade quality for size the way a video codec does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running this in the browser
&lt;/h2&gt;

&lt;p&gt;That chain is plain ffmpeg, and ffmpeg can be compiled to WebAssembly. ffmpeg.wasm runs the same filters in the browser from the same command line.&lt;/p&gt;

&lt;p&gt;The cost is upfront: the engine is around 30 MB and gets fetched on first use. After that it is cached and the conversion works offline. The benefit is that the video never leaves the machine. There is no upload step, a large source is fine on a slow connection, and there is nothing left on a server to clean up. For internal recordings or client footage, that is usually the deciding factor.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a GIF still cannot do
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;256 colors maximum.&lt;/strong&gt; A per-video palette spends those slots well, but it cannot exceed 256. Sky gradients and subtle skin tones will still break up.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No audio.&lt;/strong&gt; The format has no audio track, in any tool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Size is brutally sensitive to duration.&lt;/strong&gt; Doubling the seconds roughly doubles the file. There is no high-quality mode that rescues a clip that is simply too long.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reducing frame rate or width is irreversible.&lt;/strong&gt; You are discarding frames and pixels, and there is no upscale back to the source.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No cropping, background removal, or text overlay.&lt;/strong&gt; A converter converts. If you need a crop or a burnt-in caption, apply it to the source video first, then convert.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If those limits break your use case, an MP4 or WebM is the right answer and a GIF is not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go next
&lt;/h2&gt;

&lt;p&gt;The full version of this write-up, with a trade-off table for the three frame rates and three widths plus a GIF versus MP4 comparison, lives at &lt;a href="https://www.aihangsoft.top/blog/convert-video-to-gif.html" rel="noopener noreferrer"&gt;https://www.aihangsoft.top/blog/convert-video-to-gif.html&lt;/a&gt;. The converter there runs entirely in your browser and is free, with no upload and no account.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>webassembly</category>
      <category>performance</category>
    </item>
    <item>
      <title>How browser-based file tools work (and why nothing gets uploaded)</title>
      <dc:creator>Aihangsoft Tools</dc:creator>
      <pubDate>Sat, 26 Sep 2026 12:05:04 +0000</pubDate>
      <link>https://dev.to/aihangsoft/how-browser-based-file-tools-work-and-why-nothing-gets-uploaded-7gl</link>
      <guid>https://dev.to/aihangsoft/how-browser-based-file-tools-work-and-why-nothing-gets-uploaded-7gl</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; When a tool says your files never leave your device, that is an architectural claim, not a marketing one. Here is the stack that makes it true, and what it costs.&lt;/p&gt;

&lt;p&gt;Most online file converters follow the same shape. Your browser uploads the file, a server runs something like ImageMagick or headless FFmpeg, and you get a download link. The work is real, and often fast. The catch is that your file left your machine, and it now sits in storage you do not control, under a retention policy you did not agree to.&lt;/p&gt;

&lt;p&gt;A browser-based tool keeps the same user experience and removes the server. Reading the file, processing it and writing the result all happen inside the tab. That is not a small variation on the upload model. It removes a whole class of problems, because there is no transfer to intercept, no queue to wait in and no retention window to think about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two architectures, same three steps
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;Server-based tool&lt;/th&gt;
&lt;th&gt;Browser-based tool&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reading the file&lt;/td&gt;
&lt;td&gt;Copied into their storage&lt;/td&gt;
&lt;td&gt;Read by the File API, stays local&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Processing&lt;/td&gt;
&lt;td&gt;Their CPU, usually queued&lt;/td&gt;
&lt;td&gt;Your CPU, inside the tab&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Result&lt;/td&gt;
&lt;td&gt;A download link that expires&lt;/td&gt;
&lt;td&gt;A Blob, ready immediately&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who can read the file&lt;/td&gt;
&lt;td&gt;Anyone with access to their storage&lt;/td&gt;
&lt;td&gt;Only you&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  WebAssembly is the reason this works at all
&lt;/h2&gt;

&lt;p&gt;WebAssembly is a compact binary format that runs at close to native speed inside a sandboxed virtual machine. Engines written in C, C++ or Rust are compiled to it and shipped as ordinary static files.&lt;/p&gt;

&lt;p&gt;The important part is that these are the real engines, not reimplementations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;qpdf&lt;/strong&gt; for PDF encryption and decryption&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FFmpeg&lt;/strong&gt; for video and audio&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tesseract&lt;/strong&gt; for OCR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;pdf-lib&lt;/strong&gt; and &lt;strong&gt;PDF.js&lt;/strong&gt; for reading and writing PDFs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OxiPNG&lt;/strong&gt; for lossless PNG optimization&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A video encoded by the FFmpeg build in your tab is FFmpeg output, not a web approximation of it. That matters more than it sounds, because the old failure mode of browser tools was that they quietly did something worse than the desktop tool you were comparing them against.&lt;/p&gt;

&lt;p&gt;Real first-load sizes, since these are files you actually download:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Engine&lt;/th&gt;
&lt;th&gt;First-load size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FFmpeg core (video and audio)&lt;/td&gt;
&lt;td&gt;about 30 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tesseract plus English data&lt;/td&gt;
&lt;td&gt;about 6.7 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;qpdf WebAssembly&lt;/td&gt;
&lt;td&gt;about 1.3 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PDF.js worker&lt;/td&gt;
&lt;td&gt;about 1.4 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That is code, not your file. It is cached after the first run, and it does not grow with the document you feed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Canvas covers everything pixel-related
&lt;/h2&gt;

&lt;p&gt;Images are the one category that does not need WebAssembly. The Canvas API exposes the same kind of drawing surface a native editor uses. createImageBitmap decodes a file, a canvas holds the pixels, and toBlob encodes the result back out to PNG, JPEG, WebP or AVIF. Cropping, resizing, watermarking and format conversion are pixel operations on that surface.&lt;/p&gt;

&lt;p&gt;For PNG output you can pair Canvas with OxiPNG, which re-compresses the file losslessly instead of throwing quality away.&lt;/p&gt;

&lt;h2&gt;
  
  
  Web Workers keep the tab responsive
&lt;/h2&gt;

&lt;p&gt;Heavy work on the main thread freezes the page, so long jobs belong in a Web Worker, a background thread with no DOM access. Video tooling hands work to FFmpeg inside a worker and receives log lines and progress events back, which is why the progress bar keeps moving while the encoder is busy.&lt;/p&gt;

&lt;p&gt;One detail worth stealing: a single-threaded Tesseract build does not need SharedArrayBuffer, and therefore does not need the COOP and COEP headers that multi-threaded WebAssembly builds usually force on your whole site. Slightly slower per page, much smaller deployment constraint.&lt;/p&gt;

&lt;h2&gt;
  
  
  File and Blob are what replace the upload
&lt;/h2&gt;

&lt;p&gt;This is the part that actually removes the network call. Slightly simplified, but this is the shape of the pipeline:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Bytes come from disk into the page. No fetch, no POST.
const bytes = new Uint8Array(await file.arrayBuffer());

// Hand them to an engine running in the tab.
const output = await engine.run(bytes);

// Build the result and hand it back as a download.
const blob = new Blob([output], { type: 'application/pdf' });
const url = URL.createObjectURL(blob);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The bytes travel from disk to memory and back. There is no code path in that loop that talks to a network. It also means there is no upload size limit imposed by a server, only the memory limit of the device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify it yourself in two minutes
&lt;/h2&gt;

&lt;p&gt;Do not take the privacy line on a tool page on faith. Check it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open DevTools, go to the Network tab, and clear it.&lt;/li&gt;
&lt;li&gt;Process a file.&lt;/li&gt;
&lt;li&gt;Look at what showed up.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You will see the page, its CSS and its JavaScript, the engine on the first run, and web fonts. You will not see your document. Analytics scripts will also appear, and they report that a page was viewed, not what was in the file.&lt;/p&gt;

&lt;p&gt;The stronger test: finish one run so the engine is loaded, then disconnect from the network and process another file without reloading. If it still works, the processing path never touched a server.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it costs
&lt;/h2&gt;

&lt;p&gt;Local processing is not free, and the honest version is worth stating:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A cold start is slow.&lt;/strong&gt; The first run downloads the engine. On a slow connection that is a real wait.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory is the ceiling.&lt;/strong&gt; Everything lives in one tab. A desktop tool streams a large file from disk, a browser tab has to hold it. Some tools publish a hard cap for exactly this reason.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A server can be faster.&lt;/strong&gt; Video encoding is CPU-bound, and a browser build falls back to software encoding rather than a hardware encoder, so it does not get the GPU speedup a native build can use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a few megabytes of PDF or a one minute clip, none of this is felt, and you get privacy plus no quotas. For a two hour 4K recording, a desktop tool is the better answer, and it is worth saying so instead of pretending otherwise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the model breaks
&lt;/h2&gt;

&lt;p&gt;There is a category of job a browser tab cannot do: anything that depends on a large model. Background removal, object erasing and generative upscaling need weights that no reasonable page load budget covers, so those run on a server and the image is uploaded. A tool that does this should say so on the page, not in a footnote.&lt;/p&gt;

&lt;p&gt;That is the useful test to apply to any tool that claims to be private. If it names a real engine and runs it in the tab, the file stays put. If it needs a bigger model than a browser can hold, it uploads, and it should admit it.&lt;/p&gt;




&lt;p&gt;This post also lives on my blog at &lt;a href="https://www.aihangsoft.top/blog/how-browser-based-file-tools-work.html" rel="noopener noreferrer"&gt;aihangsoft.top&lt;/a&gt;, where I build browser-based file tools that work this way.&lt;/p&gt;

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