split.tools has no backend for file processing — not a small one, none. Every split happens inside the browser tab. This post is the technical tour of how, for the curious and for anyone building similar tools.
Getting the file in: File API
A drag-and-drop or file-picker interaction hands the page a File object — a handle to bytes on disk, readable via arrayBuffer() without any network transfer. The OS-level open dialog is the security boundary: pages read only what you explicitly hand them. From here, each media type takes its own path.
Images: canvas
An image draws into an off-screen <canvas>, and drawImage() with source-rectangle arguments does the actual splitting — each grid cell is one draw call copying the exact pixel region into a tile-sized canvas. canvas.toBlob() then encodes PNG or JPEG using the browser's native encoder. Cut coordinates are computed against the image's true pixel dimensions (naturalWidth), so the on-screen preview scale never affects output precision.
PDFs: two libraries, zero servers
PDF needs real parsing, which is where compiled libraries running in the page come in. pdf.js — the same engine Firefox uses for its PDF viewer — renders page thumbnails onto canvases in a Web Worker, off the main thread. For the actual splitting, pdf-lib loads the document's object tree and copies selected page objects into a new document — pages aren't re-rendered, so fonts, vectors, and links pass through byte-faithful. Rebuilding the cross-reference table and writing the new file takes milliseconds.
Audio: Web Audio + hand-rolled WAV
AudioContext.decodeAudioData() turns any format the browser can play — MP3, M4A, OGG, WAV — into an AudioBuffer of raw Float32 samples. Everything downstream is arithmetic on those arrays: waveform peaks (max amplitude per pixel bucket), silence detection (RMS over 20 ms windows against a threshold), and cutting (index math — sample t × sampleRate marks the boundary). Export is a WAV encoder in ~60 lines: WAV is just a 44-byte RIFF header followed by interleaved 16-bit PCM samples. No encoding library required.
CSV and binary files: Blob.slice and never decoding
The CSV splitter and file splitter lean on the least glamorous API on the platform. File inherits from Blob, and blob.slice(start, end) returns a new Blob that refers to a byte range of the original without reading it. Slicing a 6 GB file into 100 MB parts allocates almost nothing; bytes only move when a part is saved, streamed straight from disk.
The file splitter needs nothing else — parts are plain byte ranges, which is why cat and copy /b can rejoin them. The CSV splitter has to find row boundaries first, so it reads the file in 8 MB slices and scans the raw bytes for line feeds outside double quotes. It never turns bytes into a JavaScript string: V8 caps a string at roughly 2²⁹ characters (about 537 million), so a 1 GB CSV can't even exist as one string, and decoding then re-encoding text is how accented characters get mangled. Scanning bytes avoids both problems. Each output part is then new Blob([header, file.slice(a, b)]) — still references, still no copy.
Video: captureStream + MediaRecorder
Video is the heaviest case. The splitter plays your marked segment on a <video> element while captureStream() exposes its output as a live MediaStream, and MediaRecorder encodes that stream to WebM using the browser's hardware-accelerated encoder. Frame-accurate cuts, no 30 MB WASM codec download — with the honest trade-off that capture runs in real time and re-encodes (why that's fine for social clips).
Getting files out: Blob URLs and a tiny ZIP
Results become Blobs, and URL.createObjectURL() gives each a temporary in-memory URL that a programmatic <a download> click saves to disk. Multi-file results are packaged by a small ZIP writer using STORE (no compression) — the slices are already-compressed formats, so deflate would spend CPU to save nothing. A ZIP is just local file headers, a central directory, and CRC-32 checksums; well within hand-rolling territory.
Because STORE never transforms the payload, the archive itself can be assembled from references: a 30-byte header, then the part's Blob, then the next header. The only pass over the data is computing each entry's CRC-32, done by streaming the Blob through blob.stream() in chunks — so zipping 40 CSV parts of a 2 GB export costs a CRC pass, not 2 GB of memory. The trade-off is the classic ZIP format's 32-bit size fields: without the ZIP64 extension, the archive must stay under 4 GiB. Beyond that, the tools hand you the parts individually, and the file splitter can write them straight into a folder through the File System Access API in Chromium browsers.
Why this architecture wins
No upload latency (a 2 GB file "loads" instantly), no server costs to claw back via subscriptions and watermarks, and privacy by architecture instead of policy. The web platform quietly became a capable media-processing runtime; split.tools is what building on that looks like. Try the result: image · pdf · csv · audio · video · any file.