Category: Tools & Setup

Tools & Setup is where orthogonal.info curates practical, battle-tested guides on developer productivity tools, CLI utilities, self-hosted software, and environment configuration. Whether you are bootstrapping a new development machine, evaluating self-hosted alternatives to SaaS products, or fine-tuning your terminal workflow, this category delivers step-by-step walkthroughs grounded in real-world experience. Every article is written with one goal: help you build a faster, more reliable, and more enjoyable development environment.

With over 25 in-depth posts and growing, Tools & Setup is one of the most active categories on the site — reflecting just how much time engineers spend (and save) by getting their tooling right from day one.

Key Topics Covered

Command-line productivity — Shell customization (Zsh, Fish, Starship), terminal multiplexers (tmux, Zellij), and CLI utilities like ripgrep, fd, fzf, and bat that supercharge daily workflows.
Self-hosted alternatives — Deploying and configuring tools like Gitea, Nextcloud, Vaultwarden, and Uptime Kuma so you own your data without sacrificing usability.
IDE and editor setup — Configuration guides for VS Code, Neovim, and JetBrains IDEs, including extension recommendations, keybindings, and remote development workflows.
Development environment automation — Using Ansible, Homebrew, Nix, dotfiles repositories, and container-based dev environments (Dev Containers, Devbox) to make setups reproducible.
Git workflows and tooling — Advanced Git techniques, hooks, aliases, and GUI clients that streamline version control for solo developers and teams alike.
API testing and debugging — Hands-on guides for curl, HTTPie, Postman, and browser DevTools to debug REST and GraphQL APIs efficiently.
Package and runtime management — Managing multiple language runtimes with asdf, mise, nvm, and pyenv, plus dependency management best practices.

Who This Content Is For
This category is designed for software engineers, DevOps practitioners, system administrators, and hobbyist developers who want to work smarter, not harder. Whether you are a junior developer setting up your first Linux workstation or a senior engineer optimizing a multi-machine workflow, you will find actionable advice that respects your time. The guides assume basic command-line comfort but explain advanced concepts clearly.

What You Will Learn
By exploring the articles in Tools & Setup, you will learn how to automate repetitive environment tasks so a fresh machine is productive in minutes, not days. You will discover modern CLI replacements for legacy Unix tools, understand how to evaluate self-hosted software against its SaaS equivalent, and gain confidence configuring complex development stacks. Each guide includes copy-paste commands, configuration snippets, and links to upstream documentation so you can adapt the advice to your own infrastructure.

Start browsing below to find your next productivity upgrade.

  • Self-Host Ollama: Local LLM Inference on Your Homelab

    Self-Host Ollama: Local LLM Inference on Your Homelab

    The $300/Month Problem

    📌 TL;DR: The $300/Month Problem I hit my OpenAI API billing dashboard last month and stared at $312.47. That’s what three months of prototyping a RAG pipeline cost me — and most of those tokens were wasted on testing prompts that didn’t work.
    🎯 Quick Answer: Self-hosting Ollama on a homelab with a used GPU can save over $300/month compared to OpenAI API costs. Run models like Llama 3 and Mistral locally with full data privacy and no per-token fees.

    I hit my OpenAI API billing dashboard last month and stared at $312.47. That’s what three months of prototyping a RAG pipeline cost me — and most of those tokens were wasted on testing prompts that didn’t work.

    Meanwhile, my TrueNAS box sat in the closet pulling 85 watts, running Docker containers I hadn’t touched in weeks. That’s when I started looking at Ollama — a dead-simple way to run open-source LLMs locally. No API keys, no rate limits, no surprise invoices.

    Three weeks in, I’ve moved about 80% of my development-time inference off the cloud. Here’s exactly how I set it up, what hardware actually matters, and the real performance numbers nobody talks about.

    Why Ollama Over vLLM, LocalAI, or text-generation-webui

    I tried all four. Here’s why I stuck with Ollama:

    vLLM is built for production throughput — batched inference, PagedAttention, the works. It’s also a pain to configure if you just want to ask a model a question. Setup took me 45 minutes and required building from source to get GPU support working on my machine.

    LocalAI supports more model formats (GGUF, GPTQ, AWQ) and has an OpenAI-compatible API out of the box. But the documentation is scattered, and I hit three different bugs in the Whisper integration before giving up.

    text-generation-webui (oobabooga) is great if you want a chat UI. But I needed an API endpoint I could hit from scripts and other services, and the API felt bolted on.

    Ollama won because: one binary, one command to pull a model, instant OpenAI-compatible API on port 11434. I had Llama 3.1 8B answering prompts in under 2 minutes from a cold start. That matters when you’re trying to build things, not babysit infrastructure.

    Hardware: What Actually Moves the Needle

    I’m running Ollama on a Mac Mini M2 with 16GB unified memory. Here’s what I learned about hardware that actually affects performance:

    Memory is everything. LLMs need to fit entirely in RAM (or VRAM) to run at usable speeds. A 7B parameter model in Q4_K_M quantization needs about 4.5GB. A 13B model needs ~8GB. A 70B model needs ~40GB. If the model doesn’t fit, it pages to disk and you’re looking at 0.5 tokens/second — basically unusable.

    GPU matters less than you think for models under 13B. Apple Silicon’s unified memory architecture means the M1/M2/M3 chips run these models surprisingly well — I get 35-42 tokens/second on Llama 3.1 8B with my M2. A dedicated NVIDIA GPU is faster (an RTX 3090 with 24GB VRAM will push 70+ tok/s on the same model), but the Mac Mini uses 15 watts doing it versus 350+ watts for the 3090.

    CPU-only is viable for small models. On a 4-core Intel box with 32GB RAM, I was getting 8-12 tokens/second on 7B models. Not great for chat, but perfectly fine for batch processing, embeddings, or code review pipelines where latency doesn’t matter.

    If you’re building a homelab inference box from scratch, here’s what I’d buy today:

    • Budget ($400-600): A used Mac Mini M2 with 16GB RAM runs 7B-13B models at very usable speeds. Power draw is laughable — 15-25 watts under inference load.
    • Mid-range ($800-1200): A Mac Mini M4 with 32GB lets you run 30B models and keeps two smaller models hot in memory. The M4 with 32GB unified memory is the sweet spot for most homelab setups.
    • GPU path ($500-900): If you already have a Linux box, grab a used RTX 3090 24GB — they’ve dropped to $600-800 and the 24GB VRAM handles 13B models at 70+ tok/s. Just make sure your PSU can handle the 350W draw.

    The Setup: 5 Minutes, Not Kidding

    On macOS or Linux:

    curl -fsSL https://ollama.com/install.sh | sh
    ollama serve &
    ollama pull llama3.1:8b

    That’s it. The model downloads (~4.7GB for the Q4_K_M quantized 8B), and you’ve got an API running on localhost:11434.

    Test it:

    curl http://localhost:11434/api/generate -d '{
      "model": "llama3.1:8b",
      "prompt": "Explain TCP three-way handshake in two sentences.",
      "stream": false
    }'

    For Docker (which is what I use on TrueNAS):

    docker run -d \
      --name ollama \
      -v ollama_data:/root/.ollama \
      -p 11434:11434 \
      --restart unless-stopped \
      ollama/ollama:latest

    Then pull your model into the running container:

    docker exec ollama ollama pull llama3.1:8b

    Real Benchmarks: What I Actually Measured

    I ran each model 10 times with the same prompt (“Write a Python function to merge two sorted lists with O(n) complexity, with docstring and type hints”) and averaged the results. Mac Mini M2, 16GB, nothing else running:

    Model Size (Q4_K_M) Tokens/sec Time to first token RAM used
    Llama 3.1 8B 4.7GB 38.2 0.4s 5.1GB
    Mistral 7B v0.3 4.1GB 41.7 0.3s 4.6GB
    CodeLlama 13B 7.4GB 22.1 0.8s 8.2GB
    Llama 3.1 70B (Q2_K) 26GB 3.8 4.2s 28GB*

    *The 70B model technically ran on 16GB with aggressive quantization but spent half its time swapping. I wouldn’t recommend it without 32GB+ RAM.

    For context: GPT-4o through the API typically returns 50-80 tokens/second, but you’re paying per token and dealing with rate limits. 38 tokens/second from a local 8B model is fast enough that you barely notice the difference when coding.

    Making It Useful: The OpenAI-Compatible API

    This is the part that made Ollama actually practical for me. It exposes an OpenAI-compatible endpoint at /v1/chat/completions, which means you can point any tool that uses the OpenAI SDK at your local instance by just changing the base URL:

    from openai import OpenAI
    
    client = OpenAI(
        base_url="http://192.0.2.10:11434/v1",
        api_key="[REDACTED]"  # Ollama doesn't require auth
    )
    
    response = client.chat.completions.create(
        model="llama3.1:8b",
        messages=[{"role": "user", "content": "Review this PR diff..."}]
    )
    print(response.choices[0].message.content)

    I use this for:

    • Automated code review — a git hook sends diffs to the local model before I push
    • Log analysis — pipe structured logs through a prompt that flags anomalies
    • Documentation generation — point it at a module and get decent first-draft docstrings
    • Embedding generationollama pull nomic-embed-text gives you a solid embedding model for RAG without paying per-token

    None of these need GPT-4 quality. A well-prompted 8B model handles them at 90%+ accuracy, and the cost is literally zero per request.

    Gotchas I Hit (So You Don’t Have To)

    Memory pressure kills everything. When Ollama loads a model, it stays in memory until another model evicts it or you restart the service. If you’re running other containers on the same box, set OLLAMA_MAX_LOADED_MODELS=1 to prevent two 8GB models from eating all your RAM and triggering the OOM killer.

    Network binding matters. By default Ollama only listens on 127.0.0.1:11434. If you want other machines on your LAN to use it (which is the whole point of a homelab setup), set OLLAMA_HOST=0.0.0.0. But don’t expose this to the internet — there’s no auth layer. Put it behind a reverse proxy with basic auth or Tailscale if you need remote access.

    Quantization matters more than model size. A 13B model at Q4_K_M often beats a 7B at Q8. The sweet spot for most use cases is Q4_K_M — it’s roughly 4 bits per weight, which keeps quality surprisingly close to full precision while cutting memory by 4x.

    Context length eats memory fast. The default context window is 2048 tokens. Bumping it to 8192 with ollama run llama3.1 --ctx-size 8192 roughly doubles memory usage. Plan accordingly.

    When to Stay on the Cloud

    I still use GPT-4o and Claude for anything requiring deep reasoning, long context, or multi-step planning. Local 8B models are not good at complex architectural analysis or debugging subtle race conditions. They’re excellent at well-scoped, repetitive tasks with clear instructions.

    The split I’ve landed on: cloud APIs for thinking, local models for doing. My API bill dropped from $312/month to about $45.

    What I’d Do Next

    If your homelab already runs Docker, adding Ollama takes 5 minutes and costs nothing. Start with llama3.1:8b for general tasks and nomic-embed-text for embeddings. If you find yourself using it daily (you will), consider dedicated hardware — a Mac Mini or a used GPU that stays on 24/7.

    The models are improving fast. Llama 3.1 8B today is better than Llama 2 70B was a year ago. By the time you read this, there’s probably something even better on Ollama’s model library. Pull it and try it — that’s the beauty of running your own inference server.

    Related Reading

    Full disclosure: Hardware links above are affiliate links.


    📡 Want daily market intelligence with the same no-BS approach? Join Alpha Signal on Telegram for free daily signals and analysis.

    References

    1. Ollama — “Ollama Documentation”
    2. GitHub — “LocalAI: OpenAI-Compatible API for Local Models”
    3. GitHub — “vLLM: A High-Throughput and Memory-Efficient Inference and Serving Library for LLMs”
    4. TrueNAS — “TrueNAS Documentation Hub”
    5. Docker — “Docker Official Documentation”

    Frequently Asked Questions

    What is Self-Host Ollama: Local LLM Inference on Your Homelab about?

    The $300/Month Problem I hit my OpenAI API billing dashboard last month and stared at $312.47. That’s what three months of prototyping a RAG pipeline cost me — and most of those tokens were wasted on

    Who should read this article about Self-Host Ollama: Local LLM Inference on Your Homelab?

    Anyone interested in learning about Self-Host Ollama: Local LLM Inference on Your Homelab and related topics will find this article useful.

    What are the key takeaways from Self-Host Ollama: Local LLM Inference on Your Homelab?

    Meanwhile, my TrueNAS box sat in the closet pulling 85 watts, running Docker containers I hadn’t touched in weeks. That’s when I started looking at Ollama — a dead-simple way to run open-source LLMs l

  • HashForge: Privacy-First Hash Generator for All Algos

    HashForge: Privacy-First Hash Generator for All Algos

    I’ve been hashing things for years — verifying file downloads, generating checksums for deployments, creating HMAC signatures for APIs. And every single time, I end up bouncing between three or four browser tabs because no hash tool does everything I need in one place.

    So I built HashForge.

    The Problem with Existing Hash Tools

    📌 TL;DR: I’ve been hashing things for years — verifying file downloads, generating checksums for deployments, creating HMAC signatures for APIs. And every single time, I end up bouncing between three or four browser tabs because no hash tool does everything I need in one place. So I built HashForge .
    Quick Answer: HashForge is a free, privacy-first hash generator that computes MD5, SHA-1, SHA-256, SHA-512, and HMAC simultaneously — entirely in your browser with zero server uploads. It replaces multiple single-algorithm tools with one unified interface.

    Here’s what frustrated me about the current space. Most online hash generators force you to pick one algorithm at a time. Need MD5 and SHA-256 for the same input? That’s two separate page loads. Browserling’s tools, for example, have a different page for every algorithm — MD5 on one URL, SHA-256 on another, SHA-512 on yet another. You’re constantly copying, pasting, and navigating.

    Then there’s the privacy problem. Some hash generators process your input on their servers. For a tool that developers use with sensitive data — API keys, passwords, config files — that’s a non-starter. Your input should never leave your machine.

    And finally, most tools feel like they were built in 2010 and never updated. No dark mode, no mobile responsiveness, no keyboard shortcuts. They work, but they feel dated.

    What Makes HashForge Different

    All algorithms at once. Type or paste text, and you instantly see MD5, SHA-1, SHA-256, SHA-384, and SHA-512 hashes side by side. No page switching, no dropdown menus. Every algorithm, every time, updated in real-time as you type.

    Four modes in one tool. HashForge isn’t just a text hasher. It has four distinct modes:

    • Text mode: Real-time hashing as you type. Supports hex, Base64, and uppercase hex output.
    • File mode: Drag-and-drop any file — PDFs, ISOs, executables, anything. The file never leaves your browser. There’s a progress indicator for large files and it handles multi-gigabyte files using the Web Crypto API’s native streaming.
    • HMAC mode: Enter a secret key and message to generate HMAC signatures for SHA-1, SHA-256, SHA-384, and SHA-512. Essential for API development and webhook verification.
    • Verify mode: Paste two hashes and instantly compare them. Uses constant-time comparison to prevent timing attacks — the same approach used in production authentication systems.

    100% browser-side processing. Nothing — not a single byte — leaves your browser. HashForge uses the Web Crypto API for SHA algorithms and a pure JavaScript implementation for MD5 (since the Web Crypto API doesn’t support MD5). There’s no server, no analytics endpoint collecting your inputs, no “we process your data according to our privacy policy” fine print. Your data stays on your device, period.

    Technical Deep Dive

    HashForge is a single HTML file — 31KB total with all CSS and JavaScript inline. Zero external dependencies. No frameworks, no build tools, no CDN requests. This means:

    • First paint under 100ms on any modern browser
    • Works offline after the first visit (it’s a PWA with a service worker)
    • No supply chain risk — there’s literally nothing to compromise

    The MD5 Challenge

    The Web Crypto API supports SHA-1, SHA-256, SHA-384, and SHA-512 natively, but not MD5. Since MD5 is still widely used for file verification (despite being cryptographically broken), I implemented it in pure JavaScript. The implementation handles the full MD5 specification — message padding, word array conversion, and all four rounds of the compression function.

    Is MD5 secure? No. Should you use it for passwords? Absolutely not. But for verifying that a file downloaded correctly? It’s fine, and millions of software projects still publish MD5 checksums alongside SHA-256 ones.

    Constant-Time Comparison

    The hash verification mode uses constant-time comparison. In a naive string comparison, the function returns as soon as it finds a mismatched character — which means comparing “abc” against “axc” is faster than comparing “abc” against “abd”. An attacker could theoretically use this timing difference to guess a hash one character at a time.

    HashForge’s comparison XORs every byte of both hashes and accumulates the result, then checks if the total is zero. The operation takes the same amount of time regardless of where (or whether) the hashes differ. This is the same pattern used in OpenSSL’s CRYPTO_memcmp and Node.js’s crypto.timingSafeEqual.

    PWA and Offline Support

    HashForge registers a service worker that caches the page on first visit. After that, it works completely offline — no internet required. The service worker uses a network-first strategy: it tries to fetch the latest version, falls back to cache if you’re offline. This means you always get updates when connected, but never lose functionality when you’re not.

    Accessibility

    Every interactive element has proper ARIA attributes. The tab navigation follows the WAI-ARIA Tabs Pattern — arrow keys move between tabs, Home/End jump to first/last. There’s a skip-to-content link for screen reader users. All buttons have visible focus states. Keyboard shortcuts (Ctrl+1 through Ctrl+4) switch between modes.

    Real-World Use Cases

    1. Verifying software downloads. You download an ISO and the website provides a SHA-256 checksum. Drop the file into HashForge’s File mode, copy the SHA-256 output, paste it into Verify mode alongside the published checksum. Instant verification.

    2. API webhook signature verification. Stripe, GitHub, and Slack all use HMAC-SHA256 to sign webhooks. When debugging webhook handlers, you can use HashForge’s HMAC mode to manually compute the expected signature and compare it against what you’re receiving. No need to write a throwaway script.

    3. Generating content hashes for ETags. Building a static site? Hash your content to generate ETags for HTTP caching. Paste the content into Text mode, grab the SHA-256, and you have a cache key.

    4. Comparing database migration checksums. After running a migration, hash the schema dump and compare it across environments. HashForge’s Verify mode makes this a two-paste operation.

    5. Quick password hash lookups. Not for security — but when you’re debugging and need to quickly check if two plaintext values produce the same hash (checking for normalization issues, encoding problems, etc.).

    What I Didn’t Build

    I deliberately left out some features that other tools include:

    • No bcrypt/scrypt/argon2. These are password hashing algorithms, not general-purpose hash functions. They’re intentionally slow and have different APIs. Mixing them in would confuse the purpose of the tool.
    • No server-side processing. Some tools offer an “API” where you POST data and get hashes back. Why? The browser can do this natively.
    • No accounts or saved history. Hash a thing, get the result, move on. If you need to save it, copy it. Simple tools should be simple.

    Try It

    HashForge is free, open-source, and runs entirely in your browser. Try it at hashforge.orthogonal.info.

    If you find it useful, support the project — it helps me keep building privacy-first tools.

    For developers: the source is on GitHub. It’s a single HTML file, so feel free to fork it, self-host it, or tear it apart to see how it works.

    Looking for more browser-based dev tools? Check out QuickShrink (image compression), PixelStrip (EXIF removal), and TypeFast (text snippets). All free, all private, all single-file.

    Looking for a great mechanical keyboard to speed up your development workflow? I’ve been using one for years and the tactile feedback genuinely helps with coding sessions. The Keychron K2 is my daily driver — compact 75% layout, hot-swappable switches, and excellent build quality. Also worth considering: a solid USB-C hub makes the multi-monitor developer setup much cleaner.

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    References

    1. NIST — “Secure Hash Standard (SHS)”
    2. OWASP — “Cryptographic Storage Cheat Sheet”
    3. GitHub — “Browserling Hash Tools Repository”
    4. RFC Editor — “RFC 3174 – US Secure Hash Algorithm 1 (SHA1)”
    5. Mozilla Developer Network — “Web Crypto API”

    Frequently Asked Questions

    What is HashForge: Privacy-First Hash Generator for All Algos about?

    I’ve been hashing things for years — verifying file downloads, generating checksums for deployments, creating HMAC signatures for APIs. And every single time, I end up bouncing between three or four b

    Who should read this article about HashForge: Privacy-First Hash Generator for All Algos?

    Anyone interested in learning about HashForge: Privacy-First Hash Generator for All Algos and related topics will find this article useful.

    What are the key takeaways from HashForge: Privacy-First Hash Generator for All Algos?

    So I built HashForge . The Problem with Existing Hash Tools Here’s what frustrated me about the current space. Most online hash generators force you to pick one algorithm at a time.

  • JSON Forge: Privacy-First JSON Formatter in Your Browser

    JSON Forge: Privacy-First JSON Formatter in Your Browser

    Pasting a nested API response into an online JSON formatter means your auth tokens, user data, and internal endpoints are now on someone else’s server. A privacy-first JSON tool that runs entirely in your browser handles the same formatting, diffing, and path-querying—without the data exfiltration risk.

    **👉 Try JSON Forge now: [jsonformatter.orthogonal.info](https://jsonformatter.orthogonal.info)** — no install, no signup, runs entirely in your browser.

    So I opened the first Google result: jsonformatter.org. Immediately hit with cookie consent banners, multiple ad blocks pushing the actual tool below the fold, and a layout so cluttered I had to squint to find the input field. I pasted my JSON — which, by the way, contained API keys and user data from a staging environment — and realized I had no idea where that data was going. Their privacy policy? Vague at best.

    Next up: JSON Editor Online. Better UI, but it wants me to create an account, upsells a paid tier, and still routes data through their servers for certain features. Then Curious Concept’s JSON Formatter — cleaner, but dated, and again: my data leaves the browser.

    I closed all three tabs and thought: I’ll just build my own.

    Introducing JSON Forge

    📌 TL;DR: Last week I needed to debug a nested API response — the kind with five levels of objects, arrays inside arrays, and keys that look like someone fell asleep on the keyboard. I just needed a JSON formatter. **👉 Try JSON Forge now: [jsonformatter.orthogonal.info](https://jsonformatter.orthogonal.
    Quick Answer: JSON Forge is a free, privacy-first JSON formatter that runs entirely in your browser — no server uploads, no accounts, no ads. It handles validation, pretty-printing, minification, and tree visualization for nested API responses in one clean interface.

    JSON Forge is a privacy-first JSON formatter, viewer, and editor that runs entirely in your browser. No servers. No tracking. No accounts. Your data never leaves your machine — period.

    I designed it around the way I actually work with JSON: paste it in, format it, find the key I need, fix the typo, copy it out. Keyboard-driven, zero friction, fast. Here’s what it does:

    • Format & Minify — One-click pretty-print or compact output, with configurable indentation
    • Sort Keys — Alphabetical key sorting for cleaner diffs and easier scanning
    • Smart Auto-Fix — Handles trailing commas, unquoted keys, single quotes, and other common JSON sins that break strict parsers
    • Dual View: Code + Tree — Full syntax-highlighted code editor on the left, collapsible tree view on the right with resizable panels
    • JSONPath Navigator — Query your data with JSONPath expressions. Click any node in the tree to see its path instantly
    • Search — Full-text search across keys and values with match highlighting
    • Drag-and-Drop — Drop a .json file anywhere on the page
    • Syntax Highlighting — Color-coded strings, numbers, booleans, and nulls
    • Dark Mode — Because of course
    • Mobile Responsive — Works on tablets and phones when you need it
    • Keyboard ShortcutsCtrl+Shift+F to format, Ctrl+Shift+M to minify, Ctrl+Shift+S to sort — the workflow stays in your hands
    • PWA with Offline Support — Install it as an app, use it on a plane

    Why Client-Side Matters More Than You Think

    Here’s the thing about JSON formatters — people paste everything into them. API responses with auth tokens. Database exports with PII. Webhook payloads with customer data. Configuration files with secrets. We’ve all done it. I’ve done it a hundred times without thinking twice.

    Most online JSON tools process your input on their servers. Even the ones that claim to be “client-side” often phone home for analytics, error reporting, or feature gating. The moment your data touches a server you don’t control, you’ve introduced risk — compliance risk, security risk, and the quiet risk of training someone else’s model on your proprietary data.

    JSON Forge processes everything with JavaScript in your browser tab. Open DevTools, watch the Network tab — you’ll see zero outbound requests after the initial page load. I’m not asking you to trust my word; I’m asking you to verify it yourself. The code is right there.

    The Single-File Architecture

    One of the more unusual decisions I made: JSON Forge is a single HTML file. All the CSS, all the JavaScript, every feature — packed into roughly 38KB total. No build step. No npm install. No webpack config. No node_modules black hole.

    Why? A few reasons:

    1. Portability. You can save the file to your desktop and run it offline forever. Email it to a colleague. Put it on a USB drive. It just works.
    2. Auditability. One file means anyone can read the entire source in an afternoon. No dependency trees to trace, no hidden packages, no supply chain risk. Zero dependencies means zero CVEs from upstream.
    3. Performance. No framework overhead. No virtual DOM diffing. No hydration step. It loads instantly and runs at the speed of vanilla JavaScript.
    4. Longevity. Frameworks come and go. A single HTML file with vanilla JS will work in browsers a decade from now, the same way it works today.

    I won’t pretend it was easy to keep everything in one file as features grew. But the constraint forced better decisions — leaner code, no unnecessary abstractions, every byte justified.

    The Privacy-First Toolkit

    JSON Forge is actually part of a broader philosophy I’ve been building around: developer tools that respect your data by default. If you share that mindset, you might also find these useful:

    • QuickShrink — A browser-based image compressor. Resize and compress images without uploading them anywhere. Same client-side architecture.
    • PixelStrip — Strips EXIF metadata from photos before you share them. GPS coordinates, camera info, timestamps — gone, without ever leaving your browser.
    • HashForge — A privacy-first hash generator supporting MD5, SHA-1, SHA-256, SHA-512, and more. Hash files and text locally with zero server involvement.

    Every tool in this collection follows the same rules: no server processing, no tracking, no accounts, works offline. The way developer tools should be.

    What’s Under the Hood

    For the technically curious, here’s a peek at how some of the features work:

    The auto-fix engine runs a series of regex-based transformations and heuristic passes before attempting JSON.parse(). It handles the most common mistakes I’ve seen in the wild — trailing commas after the last element, single-quoted strings, unquoted property names, and even some cases of missing commas between elements. It won’t fix deeply broken structures, but it catches the 90% case that makes you mutter “where’s the typo?” for ten minutes.

    The tree view is built by recursively walking the parsed object and generating DOM nodes. Each node is collapsible, shows the data type and child count, and clicking it copies the full JSONPath to that element. It stays synced with the code view — edit the raw JSON, the tree updates; click in the tree, the code highlights.

    The JSONPath navigator uses a lightweight evaluator I wrote rather than pulling in a library. It supports bracket notation, dot notation, recursive descent ($..), and wildcard selectors — enough for real debugging work without the weight of a full spec implementation.

    Developer Setup & Gear

    I spend most of my day staring at JSON, logs, and API responses. If you’re the same, investing in your workspace makes a real difference. Here’s what I use and recommend:

    • LG 27″ 4K UHD Monitor — Sharp text, accurate colors, and enough resolution to have a code editor, tree view, and terminal side by side without squinting.
    • Keychron Q1 HE Mechanical Keyboard — Hall effect switches, programmable layers, and a typing feel that makes long coding sessions genuinely comfortable.
    • Anker USB-C Hub — One cable to connect the monitor, keyboard, and everything else to my laptop. Clean desk, clean mind.

    (Affiliate links — buying through these supports my work on free, open-source tools at no extra cost to you.)

    Try It, Break It, Tell Me What’s Missing

    JSON Forge is free, open, and built for developers who care about their data. I use it daily — it’s replaced every other JSON tool in my workflow. But I’m one person with one set of use cases, and I know there are features and edge cases I haven’t thought of yet.

    Give it a try at orthogonal.info/json-forge. Paste in the gnarliest JSON you’ve got. Try the auto-fix on something that’s almost-but-not-quite valid. Explore the tree view on a deeply nested response. Install it as a PWA and use it offline.

    If something breaks, if you want a feature, or if you just want to say hey — I’d love to hear from you. And if JSON Forge saves you even five minutes of frustration, consider buying me a coffee. It keeps the lights on and the tools free. ☕

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    References

    1. Mozilla Developer Network — “Working with JSON”
    2. OWASP — “OWASP Top Ten Privacy Risks”
    3. RFC Editor — “RFC 8259: The JavaScript Object Notation (JSON) Data Interchange Format”
    4. GitHub — “JSON Formatter and Validator”
    5. NIST — “Guide to Protecting the Confidentiality of Personally Identifiable Information (PII)”

    Frequently Asked Questions

    What is JSON Forge: Privacy-First JSON Formatter in Your Browser about?

    Last week I needed to debug a nested API response — the kind with five levels of objects, arrays inside arrays, and keys that look like someone fell asleep on the keyboard. I just needed a JSON format

    Who should read this article about JSON Forge: Privacy-First JSON Formatter in Your Browser?

    Anyone interested in learning about JSON Forge: Privacy-First JSON Formatter in Your Browser and related topics will find this article useful.

    What are the key takeaways from JSON Forge: Privacy-First JSON Formatter in Your Browser?

    **👉 Try JSON Forge now: [jsonformatter.orthogonal.info](https://jsonformatter.orthogonal.info)** — no install, no signup, runs entirely in your browser. So I opened the first Google result: jsonformat

  • Parse JPEG EXIF Data in Browser With Zero Dependencies

    Parse JPEG EXIF Data in Browser With Zero Dependencies

    Parsing JPEG EXIF data in the browser without dependencies means reading a binary format—TIFF-structured IFDs, big-endian and little-endian byte orders, and tag types that reference offset chains. Most tutorials hand-wave this complexity, but if you want zero-dependency EXIF extraction, you need to understand the byte layout.

    Why Parse EXIF Data in the Browser?

    📌 TL;DR: Last year I built PixelStrip , a browser-based tool that reads and strips EXIF metadata from photos. When I started, I assumed I’d pull in exifr or piexifjs and call it a day.
    Quick Answer: Parse JPEG EXIF data in the browser by reading binary markers (0xFFD8 for SOI, 0xFFE1 for APP1), navigating the TIFF/IFD structure with DataView, and extracting tag values using the EXIF specification — no external libraries needed, under 5KB of JavaScript.

    Server-side EXIF parsing is trivial — exiftool handles everything. But uploading photos to a server defeats the purpose if your goal is privacy. The whole point of PixelStrip is that your photos never leave your device. That means the parser must run in JavaScript, in the browser, with no network calls.

    Libraries like exif-js (2.3MB minified, last updated 2019) and piexifjs (89KB but ships with known bugs around IFD1 parsing) exist. But for a single-file webapp where every kilobyte counts, writing a focused parser that handles exactly the tags we need — GPS, camera model, timestamps, orientation — came out smaller and faster.

    JPEG File Structure: The 60-Second Version

    A JPEG file is a sequence of markers. Each marker starts with 0xFF followed by a marker type byte. The ones that matter for EXIF:

    This is the 60-second version. If you want the full byte-by-byte walkthrough of where GPS coordinates actually live inside these markers — the IFD chains, rational encoding, and how to read lat/long from the raw bytes — I tore a JPEG apart byte by byte here.

    FF D8 → SOI (Start of Image) — always the first two bytes
    FF E1 [len] → APP1 — this is where EXIF data lives
    FF E0 [len] → APP0 — JFIF header (we skip this)
    FF DB [len] → DQT (Quantization table)
    FF C0 [len] → SOF0 (Start of Frame — image dimensions)
    ...
    FF D9 → EOI (End of Image)
    

    The key insight: EXIF data is just a TIFF file embedded inside the APP1 marker. Once you find FF E1, skip 2 bytes for the length field and 6 bytes for the string Exif\0\0, and you’re looking at a standard TIFF header.

    Step 1: Find the APP1 Marker

    Here’s how to locate it. We use a DataView over an ArrayBuffer — the browser’s native tool for reading binary data:

    function findAPP1(buffer) {
     const view = new DataView(buffer);
    
     // Verify JPEG magic bytes
     if (view.getUint16(0) !== 0xFFD8) {
     throw new Error('Not a JPEG file');
     }
    
     let offset = 2;
     while (offset < view.byteLength - 1) {
     const marker = view.getUint16(offset);
    
     if (marker === 0xFFE1) {
     // Found APP1 — return offset past the marker
     return offset + 2;
     }
    
     if ((marker & 0xFF00) !== 0xFF00) {
     break; // Not a valid marker, bail
     }
    
     // Skip to next marker: 2 bytes marker + length field value
     const segLen = view.getUint16(offset + 2);
     offset += 2 + segLen;
     }
    
     return -1; // No EXIF found
    }
    

    This runs in under 0.1ms on a 10MB file because we’re only scanning marker headers, not reading pixel data.

    Step 2: Parse the TIFF Header

    Inside APP1, after the Exif\0\0 prefix, you hit a TIFF header. The first two bytes tell you the byte order:

    • 0x4949 (“II”) → Intel byte order (little-endian) — used by most smartphones
    • 0x4D4D (“MM”) → Motorola byte order (big-endian) — used by some Nikon/Canon DSLRs

    This is the gotcha that trips up every first-time EXIF parser writer. If you hardcode endianness, your parser works on iPhone photos but breaks on Canon RAW files (or vice versa). You must pass the littleEndian flag to every DataView call:

    function parseTIFFHeader(view, tiffStart) {
     const byteOrder = view.getUint16(tiffStart);
     const littleEndian = byteOrder === 0x4949;
    
     // Verify TIFF magic number (42)
     const magic = view.getUint16(tiffStart + 2, littleEndian);
     if (magic !== 0x002A) {
     throw new Error('Invalid TIFF header');
     }
    
     // Offset to first IFD, relative to TIFF start
     const ifdOffset = view.getUint32(tiffStart + 4, littleEndian);
     return { littleEndian, firstIFD: tiffStart + ifdOffset };
    }
    

    Step 3: Walk the IFD (Image File Directory)

    An IFD is just a flat array of 12-byte entries. Each entry has:

    Bytes 0-1: Tag ID (e.g., 0x0112 = Orientation)
    Bytes 2-3: Data type (1=byte, 2=ASCII, 3=short, 5=rational...)
    Bytes 4-7: Count (number of values)
    Bytes 8-11: Value (if ≤4 bytes) or offset to value (if >4 bytes)
    

    The tags we care about for privacy:

    Tag ID Name Why It Matters
    0x010F Make Device manufacturer
    0x0110 Model Exact phone/camera model
    0x0112 Orientation How to rotate the image
    0x0132 DateTime When photo was modified
    0x8825 GPSInfoIFD Pointer to GPS sub-IFD
    0x9003 DateTimeOriginal When photo was taken

    Here’s the IFD walker:

    function readIFD(view, ifdStart, littleEndian, tiffStart) {
     const entries = view.getUint16(ifdStart, littleEndian);
     const tags = {};
    
     for (let i = 0; i < entries; i++) {
     const entryOffset = ifdStart + 2 + (i * 12);
     const tag = view.getUint16(entryOffset, littleEndian);
     const type = view.getUint16(entryOffset + 2, littleEndian);
     const count = view.getUint32(entryOffset + 4, littleEndian);
    
     tags[tag] = readTagValue(view, entryOffset + 8,
     type, count, littleEndian, tiffStart);
     }
    
     return tags;
    }
    

    Step 4: Extract GPS Coordinates

    GPS data lives in its own sub-IFD, pointed to by tag 0x8825. The coordinates are stored as rational numbers — pairs of 32-bit integers representing numerator and denominator. Latitude 47° 36′ 22.8″ is stored as three rationals: 47/1, 36/1, 228/10.

    function readRational(view, offset, littleEndian) {
     const num = view.getUint32(offset, littleEndian);
     const den = view.getUint32(offset + 4, littleEndian);
     return den === 0 ? 0 : num / den;
    }
    
    function gpsToDecimal(degrees, minutes, seconds, ref) {
     let decimal = degrees + minutes / 60 + seconds / 3600;
     if (ref === 'S' || ref === 'W') decimal = -decimal;
     return Math.round(decimal * 1000000) / 1000000;
    }
    

    When I tested this against 500 photos from five different phone models (iPhone 15, Pixel 8, Samsung S24, OnePlus 12, Xiaomi 14), GPS parsing succeeded on 100% of photos that had location services enabled. The coordinates matched exiftool output to 6 decimal places every time.

    Step 5: Strip It All Out

    Stripping EXIF is conceptually simpler than reading it. You have two options:

    1. Nuclear option: Remove the entire APP1 segment. Copy bytes before FF E1, skip the segment, copy everything after. Result: zero metadata, ~15KB smaller file. But you lose the Orientation tag, which means some photos display rotated.
    2. Surgical option (what PixelStrip uses): Keep the Orientation tag (0x0112), zero out everything else. This means nulling the GPS sub-IFD, blanking ASCII strings (Make, Model, DateTime), and zeroing rational values — without changing any offsets or lengths.

    The surgical approach is harder to implement but produces better results. Users don’t want their photos suddenly displaying sideways.

    Performance: How Fast Is Pure JS Parsing?

    I benchmarked the parser against exifr (the current best JS EXIF library) on 100 photos ranging from 1MB to 12MB:

    Metric Custom Parser exifr
    Bundle size 2.8KB (minified) 44KB (minified, JPEG-only build)
    Parse time (avg) 0.3ms 1.2ms
    Memory allocation ~4KB per parse ~18KB per parse
    GPS accuracy 6 decimal places 6 decimal places

    The custom parser is 4x faster because it skips tags we don’t need. exifr is a general-purpose library that parses everything — MakerNotes, XMP, IPTC — which is great if you need those, overkill if you don’t.

    The Gotchas I Hit (So You Don’t Have To)

    1. Samsung’s non-standard MakerNote offsets. Samsung phones embed a proprietary MakerNote block that uses absolute offsets instead of TIFF-relative offsets. If your IFD walker follows pointers naively, you’ll read garbage data. Solution: bound-check every offset against the APP1 segment length before dereferencing.

    2. Thumbnail images contain their own EXIF data. IFD1 (the second IFD) often contains a JPEG thumbnail — and that thumbnail can have its own APP1 with GPS data. If you strip the main EXIF but forget the thumbnail, you’ve accomplished nothing. Always scan the full APP1 for nested JPEG markers.

    3. Photos edited in Photoshop have XMP metadata too. XMP is a separate XML-based metadata format stored in a different APP1 segment (identified by the http://ns.adobe.com/xap/1.0/ prefix instead of Exif\0\0). A complete metadata stripper needs to handle both.

    Try It Yourself

    The complete parser is about 150 lines of JavaScript. If you want to see it in action — drop a photo into PixelStrip and click “Show Details” to see every EXIF tag before stripping. The EXIF data guide explains why this matters for privacy.

    If you’re building your own tools and want a solid development setup, a 16GB RAM developer laptop handles browser-based binary parsing without breaking a sweat. For heavier workloads — batch processing thousands of images — consider a 32GB desktop setup or an external SSD for fast file I/O.

    What I’d Do Differently

    If I were starting over, I’d use ReadableStream with BYOB readers instead of loading the entire file into an ArrayBuffer. For a 15MB photo, the current approach allocates 15MB of memory upfront. With streaming, you could parse the EXIF data (which lives in the first few KB) and abort the read early — important for mobile devices with tight memory budgets.

    The JPEG format is 32 years old and showing its age. But for now, it’s still 73% of all images on the web (per HTTP Archive, February 2026), and EXIF is baked into every one of them. Understanding the binary format isn’t just an academic exercise — it’s the foundation for building privacy tools that actually work.

    Related reading:

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    Frequently Asked Questions

    How do I read EXIF data from a JPEG in the browser?

    You can parse JPEG EXIF data entirely in the browser using JavaScript without any external libraries. By reading the binary file with FileReader and parsing the TIFF header and IFD entries, you can extract metadata like camera model, GPS coordinates, and timestamps.

    What is EXIF data in a JPEG file?

    EXIF (Exchangeable Image File Format) data is metadata embedded in JPEG images by cameras and phones. It includes information like the date taken, camera settings (aperture, shutter speed, ISO), GPS coordinates, and device model.

    Why parse EXIF data without external dependencies?

    Zero-dependency EXIF parsing keeps your bundle size small and eliminates supply chain security risks from third-party packages. It also gives you full control over which metadata fields to extract and how to handle edge cases in malformed files.

    Can browser-based EXIF parsing handle large image files?

    Yes, since EXIF data is stored in the first few kilobytes of a JPEG file, you only need to read the beginning of the file. Using FileReader with an ArrayBuffer slice, you can extract metadata from multi-megabyte images almost instantly without loading the full image into memory.

    References

    1. ExifTool by Phil Harvey — “ExifTool Documentation”
    2. Mozilla Developer Network (MDN) — “JPEG Image File Format”
    3. GitHub — “exif-js: JavaScript Library for Reading EXIF Metadata”
    4. GitHub — “piexifjs: Read and Modify EXIF in Client-Side JavaScript”
    5. International Telecommunication Union (ITU) — “ITU-T Recommendation T.81: Digital Compression and Coding of Continuous-Tone Still Images (JPEG)”

  • I Benchmarked 5 Image Compressors With the Same 10 Photos

    I Benchmarked 5 Image Compressors With the Same 10 Photos

    I ran the same 10 images through five different online compressors and measured everything: output file size, visual quality loss, compression speed, and what happened to my data. Two of the five uploaded my photos to servers in jurisdictions I couldn’t identify. One silently downscaled my images. And the one that kept everything local — QuickShrink — actually produced competitive results.

    Here’s the full breakdown.

    The Test Setup

    📌 TL;DR: I ran the same 10 images through five different online compressors and measured everything: output file size, visual quality loss, compression speed, and what happened to my data. Two of the five uploaded my photos to servers in jurisdictions I couldn’t identify. One silently downscaled my images.
    Quick Answer: Among the five image compressors tested, QuickShrink delivered competitive compression ratios (60-75% size reduction) while being the only tool that processes images entirely in your browser — two competitors silently uploaded photos to unidentified servers, and one secretly downscaled images.

    I selected 10 JPEG photos covering real-world use cases developers actually deal with:

    • Product shots (3 images) — white background e-commerce photos, 3000×3000px, 4-6MB each
    • Screenshots (3 images) — IDE and terminal captures, 2560×1440px, 1-3MB each
    • Photography (2 images) — landscape shots from a Pixel 8, 4000×3000px, 5-8MB each
    • UI mockups (2 images) — Figma exports with gradients and text, 1920×1080px, 2-4MB each

    Total input: 10 files, 38.7MB combined. Target quality: 80% (the sweet spot where file size drops dramatically but human eyes can’t reliably spot the difference).

    The five compressors tested:

    1. TinyPNG — the default most developers reach for
    2. Squoosh — Google’s open-source option (squoosh.app)
    3. Compressor.io — popular alternative with multiple format support
    4. iLoveIMG — widely recommended in “best tools” roundups
    5. QuickShrink — our browser-only compressor at tools.orthogonal.info/quickshrink

    File Size Results: Who Actually Compresses Best?

    Here’s where it gets interesting. I compressed all 10 images at roughly equivalent quality settings (80% or “medium” depending on the tool’s UI), then compared output sizes:

    Average compression ratio (smaller = better):

    • TinyPNG: 72.4% reduction (38.7MB → 10.7MB)
    • Squoosh (MozJPEG): 74.1% reduction (38.7MB → 10.0MB)
    • Compressor.io: 68.9% reduction (38.7MB → 12.0MB)
    • iLoveIMG: 61.3% reduction (38.7MB → 15.0MB)*
    • QuickShrink: 70.2% reduction (38.7MB → 11.5MB)

    *iLoveIMG’s “medium” setting is more conservative than the others. At its “extreme” setting it hit 69%, but also introduced visible banding in gradient-heavy UI mockups.

    Squoosh wins on raw compression thanks to MozJPEG, which is one of the best JPEG encoders ever written. But the margin over TinyPNG and QuickShrink is smaller than you’d expect — roughly 6-8% between the top three.

    The takeaway: for most developer workflows (blog images, documentation screenshots, product photos), the difference between 70% and 74% compression is irrelevant. You’re saving maybe 200KB per image. What matters more is everything else.

    Speed: Canvas API vs Server-Side Processing

    This is where architectures diverge. TinyPNG, Compressor.io, and iLoveIMG upload your image, process it server-side, then send back the result. Squoosh and QuickShrink process everything client-side — in your browser.

    Average time per image (including upload/download where applicable):

    • TinyPNG: 3.2 seconds (upload 1.8s + processing 0.9s + download 0.5s)
    • Squoosh: 1.4 seconds (local WebAssembly processing)
    • Compressor.io: 4.1 seconds (slower uploads, larger queue)
    • iLoveIMG: 2.8 seconds (fast CDN)
    • QuickShrink: 0.8 seconds (Canvas API, no network)

    QuickShrink is fastest because the Canvas API’s toBlob() method is essentially calling the browser’s built-in JPEG encoder, which is compiled C++ running natively. There’s no WebAssembly overhead (like Squoosh) and obviously no network round-trip (like the server-based tools).

    Here’s what the core compression looks like under the hood:

    // The heart of browser-based compression
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    canvas.width = img.naturalWidth;
    canvas.height = img.naturalHeight;
    ctx.drawImage(img, 0, 0);
    
    // This single call does all the heavy lifting
    canvas.toBlob(
     (blob) => {
     // blob is your compressed image
     // It never left your machine
     const url = URL.createObjectURL(blob);
     downloadLink.href = url;
     },
     'image/jpeg',
     0.80 // quality: 0.0 to 1.0
    );

    That’s it. The browser’s native JPEG encoder handles quantization, chroma subsampling, Huffman coding — everything. No library, no dependency, no server. The Canvas API has been stable across all major browsers since 2015.

    The Privacy Test: Where Do Your Photos Go?

    This is the part that should bother you. I ran each tool through Chrome DevTools’ Network tab to see exactly what happens when you drop an image:

    • TinyPNG: Uploads to api.tinify.com (Netherlands). Image stored temporarily. Privacy policy says files are deleted after some hours. You’re trusting their word.
    • Squoosh:100% client-side. Zero network requests during compression. Service worker caches the app for offline use.
    • Compressor.io: Uploads to their servers. I watched a 6MB photo leave my browser. Their privacy page is one paragraph.
    • iLoveIMG: Uploads to api3.ilovepdf.com. Files “deleted after 2 hours.” Servers appear to be in Spain (EU GDPR applies, which is good).
    • QuickShrink:100% client-side. Zero network requests. Works fully offline once loaded. I tested by enabling airplane mode — still works.

    If you’re compressing screenshots that contain code, terminal output, internal dashboards, or client work — server-side compression means that data hits someone else’s infrastructure. For a personal photo, maybe you don’t care. For a screenshot of your production database? You should care a lot.

    The Hidden Gotcha: Silent Downscaling

    I noticed something odd with iLoveIMG. My 4000×3000px landscape photo came back at 2000×1500px. The file was smaller, sure — but not because of better compression. It was because they halved the dimensions without telling me.

    I double-checked: there was no “resize” option enabled. Their “compress” feature silently caps images at a certain resolution on the free tier. This is a problem if you need full-resolution output for print, retina displays, or product photography.

    None of the other four tools altered image dimensions.

    When to Use What: My Honest Recommendation

    Use Squoosh when you need maximum compression and don’t mind a slightly more complex UI. The MozJPEG encoder is genuinely better than browser-native JPEG, and it supports WebP, AVIF, and other modern formats. It’s the technically superior tool.

    Use QuickShrink when you want the fastest possible workflow: drop image, download compressed version, done. No format decisions, no sliders, no settings panels. The Canvas API approach trades 3-4% compression efficiency for massive speed gains and zero complexity. I use it daily for blog images and documentation screenshots — exactly the use case where “good enough compression, instantly” beats “perfect compression, eventually.”

    Use TinyPNG when you’re batch-processing hundreds of images through their API and don’t have privacy constraints. Their WordPress plugin and CLI tools are well-maintained. At $0.009/image after the free 500, it’s cheap automation.

    Skip iLoveIMG unless you specifically need their PDF tools. The silent downscaling and middling compression don’t justify using a server-side tool when better client-side options exist.

    Skip Compressor.io — Squoosh does everything it does, client-side, with better compression.

    The Broader Point: Why Client-Side Tools Win

    The web platform in 2026 is absurdly capable. The Canvas API, WebAssembly, the File API, Service Workers — you can build tools that rival desktop apps without a single server-side dependency. And when your tool runs entirely in the user’s browser:

    • No hosting costs — static files on a CDN, done
    • No privacy liability — you never touch user data
    • No scaling problems — every user brings their own compute
    • Offline capable — works on planes, in cafes with bad wifi, wherever

    This is why I build browser-only tools. Not because client-side compression is always technically best — Squoosh’s MozJPEG proves server-grade encoders can run client-side too via WASM. But because the combination of speed, privacy, and simplicity makes it the right default for 90% of developer workflows.

    Try QuickShrink with your own images and see the numbers yourself. And if metadata privacy matters too, run those same photos through PixelStrip — it strips EXIF, GPS, and camera data the same way: entirely in your browser, with nothing uploaded anywhere. For managing code snippets without yet another Electron app, check out TypeFast.

    Tools for Your Developer Setup

    If you’re optimizing your development workflow, the right hardware makes a difference. A high-resolution monitor helps when comparing compression artifacts side-by-side (I use a 4K display and it’s the first upgrade I’d recommend). For photography workflows, a fast SD card reader eliminates the bottleneck of transferring images from camera to computer. And if you’re processing images in bulk for a client project, a portable SSD keeps your originals safe while you experiment with compression settings — never compress your only copy.

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    References

    1. Google — “Squoosh: An open-source image compression web app”
    2. TinyPNG — “TinyPNG: Compress PNG images while preserving transparency”
    3. Compressor.io — “Compressor.io: Optimize and compress your images online”
    4. Mozilla Developer Network (MDN) — “Image optimization: Best practices for web performance”
    5. GitHub — “Squoosh GitHub Repository”

    Frequently Asked Questions

    What is I Benchmarked 5 Image Compressors With the Same 10 Photos about?

    I ran the same 10 images through five different online compressors and measured everything: output file size, visual quality loss, compression speed, and what happened to my data. Two of the five uplo

    Related Privacy Tools

    Who should read this article about I Benchmarked 5 Image Compressors With the Same 10 Photos?

    Anyone interested in learning about I Benchmarked 5 Image Compressors With the Same 10 Photos and related topics will find this article useful.

    What are the key takeaways from I Benchmarked 5 Image Compressors With the Same 10 Photos?

    One silently downscaled my images. And the one that kept everything local — QuickShrink — actually produced competitive results. Here’s the full breakdown.

  • Pomodoro Technique Works Better With Gamified Timers

    Pomodoro Technique Works Better With Gamified Timers

    The Pomodoro Technique — work for 25 minutes, break for 5 — has been around since 1987. The science backs it up: time-boxing reduces procrastination and improves focus. But here’s the problem: most people try it for three days and quit. Not because the technique fails, but because a plain countdown timer gives you zero reason to come back tomorrow.

    Why Streaks Change Everything

    📌 TL;DR: The Pomodoro Technique — work for 25 minutes, break for 5 — has been around since 1987. The science backs it up: time-boxing reduces procrastination and improves focus. But here’s the problem: most people try it for three days and quit.
    Quick Answer: The Pomodoro Technique works better when combined with gamification — daily streaks, XP systems, and progress tracking exploit loss aversion to keep you coming back. FocusForge adds these mechanics to the classic 25/5 timer to sustain long-term focus habits.

    Duolingo built a $12 billion company on one psychological trick: the daily streak. Miss a day and your streak resets to zero. It sounds trivial. It works because loss aversion is 2x stronger than the desire for gain (Kahneman & Tversky, 1979). You don’t open Duolingo because you love Spanish — you open it because you don’t want to lose a 47-day streak.

    The same psychology applies to focus timers. A countdown from 25:00 gives you no stakes. A countdown that says “Day 23 of your focus streak” gives you skin in the game.

    How FocusForge Applies This

    FocusForge adds three layers to the basic Pomodoro timer:

    • XP — every completed session earns experience points (25 XP for a Quick session, 75 XP for a Marathon)
    • Levels — Rookie → Apprentice → Expert → Master → Legend → Immortal. Each level has its own badge.
    • Daily Streaks — complete at least one session per day to maintain your streak. Miss a day, restart from zero.

    The actual Pomodoro technique is unchanged. You still focus for 25 minutes (or 45 or 60). But now there’s a reason to do it consistently.

    👉 Try FocusForge on Google Play — free with optional $1.99 upgrade to remove ads.

    How It Works Under the Hood

    Most Pomodoro apps are black boxes — you press start, it counts down, done. But understanding the mechanics behind gamified timers reveals why they work so well. Here’s a minimal JavaScript implementation that covers the core loop: countdown, XP reward, and streak tracking.

    class PomodoroTimer {
      constructor(minutes = 25) {
        this.duration = minutes * 60;
        this.remaining = this.duration;
        this.running = false;
        this.interval = null;
        this.onTick = null;
        this.onComplete = null;
      }
    
      start() {
        if (this.running) return;
        this.running = true;
        this.interval = setInterval(() => {
          this.remaining--;
          if (this.onTick) this.onTick(this.remaining);
          if (this.remaining <= 0) {
            this.complete();
          }
        }, 1000);
      }
    
      pause() {
        this.running = false;
        clearInterval(this.interval);
      }
    
      reset() {
        this.pause();
        this.remaining = this.duration;
      }
    
      complete() {
        this.pause();
        this.remaining = 0;
        if (this.onComplete) this.onComplete();
      }
    }
    
    // XP calculation based on session length
    function calculateXP(sessionMinutes) {
      const baseXP = sessionMinutes; // 1 XP per minute
      const bonusMultiplier = sessionMinutes >= 45 ? 1.5 : 1.0;
      return Math.floor(baseXP * bonusMultiplier);
    }
    
    // Level progression: each level requires more XP
    function getLevel(totalXP) {
      const thresholds = [
        { level: 'Rookie', xp: 0 },
        { level: 'Apprentice', xp: 100 },
        { level: 'Expert', xp: 500 },
        { level: 'Master', xp: 1500 },
        { level: 'Legend', xp: 5000 },
        { level: 'Immortal', xp: 15000 }
      ];
      let current = thresholds[0];
      for (const t of thresholds) {
        if (totalXP >= t.xp) current = t;
      }
      return current.level;
    }

    The key insight: XP calculation isn’t linear. A 45-minute Marathon session earns 67 XP (45 × 1.5), while a 25-minute Quick session earns 25 XP. That 2.7x reward ratio encourages longer focus sessions without punishing shorter ones. The level thresholds follow a roughly exponential curve — easy early wins, then progressively harder milestones. This mirrors how video games keep players engaged across hundreds of hours.

    Notice how the timer class is framework-agnostic. It uses a simple callback pattern (onTick, onComplete) so you can wire it into React, Vue, or plain DOM manipulation. In FocusForge, the timer drives both the countdown display and the XP award system — when onComplete fires, it triggers the streak check and XP deposit in a single atomic operation.

    Building My Own Timer: What I Learned

    I tried every Pomodoro app on the Play Store — Forest, Focus To-Do, Engross, Tide, and probably a dozen more. None stuck past a week. The problem wasn’t the technique. The problem was that closing the app cost me nothing. There was no consequence for abandoning a session, no reward for showing up three days in a row, no visible progress that I’d lose by quitting.

    So I built FocusForge with one rule: make quitting feel expensive. That rule comes directly from behavioral economics. Daniel Kahneman and Amos Tversky demonstrated in their 1979 Prospect Theory paper that losses feel roughly twice as painful as equivalent gains feel good. A $50 loss hurts more than a $50 win feels rewarding. The same principle applies to streaks: losing a 30-day streak feels devastating, even though the streak itself has no monetary value.

    I designed FocusForge’s streak system to maximize this loss aversion. Your streak counter is front-and-center on the home screen — you see it every time you open the app. The streak badge changes color as it grows (green at 7 days, blue at 30, purple at 100). Breaking a streak doesn’t just reset the number — it visually resets your badge to gray. That emotional punch is the feature. It’s what makes you open the app at 11:30 PM to squeeze in one more session.

    Testing with real users confirmed the theory. Before gamification, the average user completed 3.2 sessions before abandoning the app. After adding streaks and XP, the median jumped to 14 sessions — a 4.4x improvement in retention. The users who reached Level 2 (Apprentice, ~100 XP) had an 80% chance of still being active 30 days later. The level system acts as a commitment device: once you’ve invested effort earning a rank, walking away means losing that investment.

    One unexpected finding: the “streak freeze” feature — letting users protect their streak for one missed day — actually increased engagement rather than decreasing it. Users who had streak freeze available completed more sessions per week than those who didn’t. The safety net reduced anxiety about perfection, which paradoxically increased consistency. I eventually made it a reward: earn a streak freeze by completing 5 sessions in a single day.

    The Streak Algorithm

    Streak tracking sounds simple — “did the user complete a session today?” — but edge cases make it surprisingly tricky. Time zones, midnight boundaries, and offline usage all create gaps between “calendar day” and “user’s day.” Here’s the algorithm FocusForge uses, simplified for clarity:

    // Streak tracking with localStorage persistence
    const STREAK_KEY = 'focusforge_streak';
    const HISTORY_KEY = 'focusforge_history';
    
    function getStreakData() {
      const raw = localStorage.getItem(STREAK_KEY);
      return raw ? JSON.parse(raw) : { count: 0, lastDate: null };
    }
    
    function saveStreakData(data) {
      localStorage.setItem(STREAK_KEY, JSON.stringify(data));
    }
    
    function getDateString(date = new Date()) {
      // Use local date to avoid timezone issues
      return date.toLocaleDateString('en-CA'); // YYYY-MM-DD format
    }
    
    function daysBetween(dateStr1, dateStr2) {
      const d1 = new Date(dateStr1 + 'T00:00:00');
      const d2 = new Date(dateStr2 + 'T00:00:00');
      return Math.round((d2 - d1) / (1000 * 60 * 60 * 24));
    }
    
    function recordSession() {
      const today = getDateString();
      const streak = getStreakData();
    
      if (streak.lastDate === today) {
        // Already logged today — streak unchanged
        return streak;
      }
    
      const gap = streak.lastDate
        ? daysBetween(streak.lastDate, today)
        : 0;
    
      if (gap === 1) {
        // Consecutive day: increment streak
        streak.count++;
      } else if (gap > 1) {
        // Missed a day: reset to 1
        streak.count = 1;
      } else if (!streak.lastDate) {
        // First ever session
        streak.count = 1;
      }
    
      streak.lastDate = today;
      saveStreakData(streak);
    
      // Log to session history
      const history = JSON.parse(
        localStorage.getItem(HISTORY_KEY) || '[]'
      );
      history.push({ date: today, timestamp: Date.now() });
      localStorage.setItem(HISTORY_KEY, JSON.stringify(history));
    
      return streak;
    }
    
    // XP multiplier: reward consistency
    function getXPMultiplier(streakCount) {
      if (streakCount >= 100) return 3.0;
      if (streakCount >= 30)  return 2.0;
      if (streakCount >= 7)   return 1.5;
      return 1.0;
    }

    The XP multiplier is the secret sauce. At a 7-day streak, you earn 50% more XP per session. At 30 days, double. At 100 days, triple. This creates a compounding effect: the longer your streak, the faster you level up, which makes the streak even more valuable to protect. It’s a positive feedback loop that turns casual users into daily users.

    The daysBetween function uses local dates specifically to avoid the timezone trap. If a user in UTC-8 completes a session at 11 PM, that’s already the next day in UTC. Using toLocaleDateString ensures the “day” boundary matches the user’s actual experience, not the server’s clock. I learned this the hard way when early testers reported their streaks breaking at midnight despite completing sessions — they were in timezones where midnight local didn’t align with the UTC date flip.

    localStorage persistence means the streak survives browser refreshes, tab closures, and even offline periods. When the user comes back online, the algorithm looks at the gap between today and lastDate — if it’s exactly one day, the streak continues. More than one day? Reset. This keeps the system honest while being resilient to connectivity issues. For FocusForge’s mobile app (React Native), the same logic runs against AsyncStorage instead of localStorage, but the algorithm is identical.

    Related Reading

    Want to know more about FocusForge’s design and gamification mechanics? Read the full deep-dive: FocusForge: How Gamification Tricked Me Into Actually Using a Pomodoro Timer. FocusForge is part of our suite of 5 free browser tools that replace desktop apps — including NoiseLog, a sound meter app for documenting noise complaints.

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    References

    1. American Psychological Association — “Loss Aversion in Decision Making”
    2. Journal of Applied Psychology — “The Effects of Time-Boxing on Procrastination and Task Completion”
    3. Duolingo Blog — “How Streaks Keep You Learning”
    4. National Center for Biotechnology Information (NCBI) — “The Effectiveness of Gamification in Improving Focus and Task Engagement”
    5. Behavioral Science & Policy Association — “Behavioral Insights into Gamification and Productivity”

    Frequently Asked Questions

    What is Pomodoro Technique Works Better With Gamified Timers about?

    The Pomodoro Technique — work for 25 minutes, break for 5 — has been around since 1987. The science backs it up: time-boxing reduces procrastination and improves focus.

    Who should read this article about Pomodoro Technique Works Better With Gamified Timers?

    Anyone interested in learning about Pomodoro Technique Works Better With Gamified Timers and related topics will find this article useful.

    What are the key takeaways from Pomodoro Technique Works Better With Gamified Timers?

    But here’s the problem: most people try it for three days and quit. Not because the technique fails, but because a plain countdown timer gives you zero reason to come back tomorrow. Why Streaks Change

  • 5 Free Browser Tools That Replace Desktop Apps

    5 Free Browser Tools That Replace Desktop Apps

    I built 3 of these tools because I got tired of desktop apps phoning home. After 12 years as a security engineer in Big Tech, I’ve watched network traffic from “offline” desktop apps — the telemetry, the analytics pings, the “anonymous” usage data that includes your file paths and timestamps. When I needed to compress an image or strip EXIF data, I didn’t want to install yet another Electron app that ships a full Chromium browser just to resize a JPEG. So I built browser-only alternatives that do the job without ever touching a network socket.

    These five tools run entirely in your browser tab. No downloads, no accounts, no servers processing your files. And once loaded, most of them work completely offline.

    📌 TL;DR: You don’t need to install an app for everything. These browser-based tools work instantly — no download, no account, no tracking. They run entirely on your device and work offline once loaded.
    🎯 Quick Answer: Five free browser-based tools—JSON formatter, image compressor, hash generator, EXIF stripper, and snippet manager—replace desktop apps entirely. They work offline, require no downloads or accounts, and never upload your data to any server.

    1. Image Compression → QuickShrink

    Instead of installing Photoshop or GIMP just to resize an image, open QuickShrink. Drop an image, pick quality (80% is ideal), download. Compresses using the same Canvas API that powers web photo editors. Typical result: 4MB photo → 800KB with no visible difference.

    2. Photo Privacy → PixelStrip

    Before sharing photos on forums or marketplaces, strip the hidden metadata. PixelStrip shows you exactly what’s embedded (GPS, camera model, timestamps) and removes it all with one click. No upload to any server.

    3. Code Snippet Manager → TypeFast

    If you keep a file of frequently-used code blocks, email templates, or canned responses, TypeFast gives you a searchable list with one-click copy. Stores everything in your browser’s localStorage — no cloud sync needed.

    4. Focus Timer → FocusForge

    A Pomodoro timer that adds XP and streaks to make deep work addictive. Three modes: 25, 45, or 60 minutes. Level up from Rookie to Immortal. Available on Google Play for Android.

    5. Noise Meter → NoiseLog

    Turn your phone into a sound level meter that logs incidents and generates reports. Perfect for documenting noise complaints with timestamps and decibel readings. Available on Google Play.

    Why Browser-Based?

    • No install — works immediately in any browser
    • Private — data stays on your device
    • Fast — loads in milliseconds, not minutes
    • Cross-platform — works on Windows, Mac, Linux, iOS, Android
    • Offline — install as PWA for offline use

    How Browser-Only Architecture Actually Works

    Every tool in this list relies on JavaScript Web APIs that ship with modern browsers — no plugins, no downloads. But “runs in the browser” isn’t just marketing. Let me show you the actual architecture that makes these tools work offline, stay private, and perform at near-native speed.

    The Four Pillars of Client-Side Processing

    • Canvas API — renders and manipulates images pixel by pixel. This is how QuickShrink compresses photos without a server.
    • Web Audio API — captures and analyzes microphone input in real time. NoiseLog uses this to measure decibel levels.
    • File API — reads files from your local disk directly into JavaScript. No upload required. PixelStrip uses this to parse JPEG metadata.
    • localStorage / IndexedDB — persistent storage in the browser. TypeFast saves your snippets here so they survive page reloads.

    These APIs have been stable across Chrome, Firefox, Safari, and Edge for years. They’re not experimental — they’re the same foundation that powers Google Docs, Figma, and VS Code for the Web.

    Service Workers: The Offline Engine

    The secret to making browser tools work offline is the Service Worker — a background script that intercepts network requests and serves cached responses. Here’s the actual pattern I use across all my tools to enable offline functionality:

    // sw.js — Service Worker for offline-first browser tools
    const CACHE_NAME = 'tool-cache-v1';
    const ASSETS = [
      '/',
      '/index.html',
      '/app.js',
      '/style.css',
      '/manifest.json'
    ];
    
    // Cache all assets on install
    self.addEventListener('install', (event) => {
      event.waitUntil(
        caches.open(CACHE_NAME).then((cache) => cache.addAll(ASSETS))
      );
      self.skipWaiting();
    });
    
    // Clean old caches on activate
    self.addEventListener('activate', (event) => {
      event.waitUntil(
        caches.keys().then((keys) =>
          Promise.all(
            keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))
          )
        )
      );
      self.clients.claim();
    });
    
    // Serve from cache first, fall back to network
    self.addEventListener('fetch', (event) => {
      event.respondWith(
        caches.match(event.request).then((cached) => {
          return cached || fetch(event.request).then((response) => {
            // Cache new requests for next offline use
            const clone = response.clone();
            caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone));
            return response;
          });
        })
      );
    });

    Once the Service Worker caches the app assets, the tool works completely offline — airplane mode, no WiFi, doesn’t matter. The browser loads everything from its local cache. This is how QuickShrink and PixelStrip work on flights and in areas with no connectivity. No server round-trip means zero latency for the user and zero data exposure.

    localStorage: Persistent State Without a Database

    TypeFast stores all your snippets in localStorage — a simple key-value store built into every browser. Here’s the core persistence pattern:

    // Snippet storage using localStorage
    const STORAGE_KEY = 'typefast_snippets';
    
    function saveSnippets(snippets) {
      localStorage.setItem(STORAGE_KEY, JSON.stringify(snippets));
    }
    
    function loadSnippets() {
      const raw = localStorage.getItem(STORAGE_KEY);
      return raw ? JSON.parse(raw) : [];
    }
    
    function addSnippet(title, content, tags = []) {
      const snippets = loadSnippets();
      snippets.push({
        id: crypto.randomUUID(),
        title,
        content,
        tags,
        created: Date.now(),
        used: 0
      });
      saveSnippets(snippets);
    }
    
    function searchSnippets(query) {
      const q = query.toLowerCase();
      return loadSnippets().filter((s) =>
        s.title.toLowerCase().includes(q) ||
        s.content.toLowerCase().includes(q) ||
        s.tags.some((t) => t.toLowerCase().includes(q))
      );
    }

    The beauty of this approach: your data never leaves your browser’s storage directory on disk. No sync server, no account, no authentication flow. The tradeoff is that clearing browser data deletes your snippets — which is why I added an export/import feature that dumps everything to a JSON file you can back up. For most users, localStorage’s 5-10MB limit is more than enough for text snippets.

    Canvas API: GPU-Accelerated Image Processing

    Here’s the core of QuickShrink — client-side image compression in about 20 lines of JavaScript:

    async function compressImage(file, quality = 0.8) {
      const img = new Image();
      const url = URL.createObjectURL(file);
      await new Promise((resolve) => { img.onload = resolve; img.src = url; });
      URL.revokeObjectURL(url);
    
      const canvas = document.createElement('canvas');
      canvas.width = img.naturalWidth;
      canvas.height = img.naturalHeight;
      const ctx = canvas.getContext('2d');
      ctx.drawImage(img, 0, 0);
    
      const blob = await new Promise((resolve) =>
        canvas.toBlob(resolve, 'image/jpeg', quality)
      );
    
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = file.name.replace(/\.\w+$/, '-compressed.jpg');
      a.click();
    }

    The Canvas API’s toBlob() method does the heavy lifting — it re-encodes the image at whatever quality level you specify. The browser’s built-in JPEG encoder handles the compression using GPU-accelerated rendering. No library, no dependency, no server.

    WebAssembly is the next frontier. Tools like Squoosh already use WASM to run codecs like MozJPEG and AVIF directly in the browser. This means server-grade compression algorithms can now execute client-side at near-native speed. Expect browser tools to match — and eventually surpass — desktop apps for most image and video processing tasks.

    Privacy Comparison: Browser vs Desktop

    The security benefit is fundamental: your data never leaves your device. But let me be specific about what “private” actually means for each tool category. I’ve analyzed the network traffic of popular desktop alternatives using Wireshark and mitmproxy — here’s what I found:

    Tool Category Browser Tool (Privacy) Desktop App (Privacy) Data Risk
    Image Compression QuickShrink: zero network calls. File stays in browser memory, processed by Canvas API, downloaded locally. TinyPNG/Squoosh desktop: uploads image to server for processing. Photoshop: sends telemetry including file metadata to Adobe. 🔴 Server-side tools see your images. Cloud-based compressors retain copies per their ToS.
    EXIF Stripping PixelStrip: parses JPEG binary in JavaScript. GPS coordinates, camera serial numbers never leave your device. Most EXIF tools are fine locally, but online alternatives (imgonline, etc.) upload your photo — including its GPS data — to their servers. 🔴 Uploading photos with GPS data to web services exposes your home/work locations.
    Snippet Manager TypeFast: localStorage only. No sync, no cloud, no account. Data lives in browser’s SQLite-backed storage on disk. TextExpander: syncs to cloud. Alfred snippets: local but the app phones home for license checks. VS Code snippets: synced if Settings Sync enabled. 🟡 Snippet managers with cloud sync may store sensitive code, passwords, API keys on third-party servers.
    Timer/Productivity FocusForge: all data in localStorage. No usage tracking, no analytics. Toggl, Forest: track usage patterns, session times, and sync to cloud dashboards. Some sell aggregated data to enterprise customers. 🟡 Work pattern data reveals when you work, how long, what you focus on.
    Audio/Noise Meter NoiseLog: Web Audio API processes microphone locally. Recordings stay on device. Most sound meter apps request microphone + location + storage permissions. Several popular Android apps share audio fingerprints with ad networks. 🔴 Microphone access combined with location data is a serious surveillance risk.

    The pattern is clear: desktop apps and web services consistently send more data to more places than browser-only tools. The browser sandbox is a genuine security boundary — a browser tab can’t access your filesystem, can’t read other tabs, and can’t make network requests to arbitrary endpoints without CORS headers. This isn’t just theory; it’s enforced by the browser engine at the OS level.

    My Daily Workflow: How I Actually Use These Tools

    Here’s how these tools fit into my actual work week as a security engineer who runs a homelab and writes a technical blog:

    Morning (blog writing): I write 2-3 technical articles a week for orthogonal.info. Every article needs screenshots — terminal output, dashboard views, architecture diagrams. I take screenshots at native resolution (3024×1964 on my MacBook), which produces 3-5MB PNGs. Before uploading to WordPress, I open QuickShrink, drop all images in, compress to 80% quality. Total time: 15 seconds. Total data sent to external servers: zero bytes. My blog loads faster because images are 800KB instead of 4MB, and I never have to wonder if some compression service is caching my screenshots of internal tools.

    Selling gear on forums: Whenever I sell old hardware on Reddit or local forums, I photograph the items and run every photo through PixelStrip before posting. Last month I checked a listing photo’s EXIF data — it contained my exact GPS coordinates (accurate to 10 meters), the time the photo was taken, and my phone model. That’s enough for a motivated buyer to know exactly where I live and when I’m home. One click in PixelStrip strips all of that. I’ve made this a habit for any photo I share publicly.

    During coding sessions: TypeFast holds my most-used snippets — kubectl commands for my homelab, curl templates for API testing, SQL queries I run frequently against my trading database. When I’m writing a new Kubernetes manifest, I search “helm” in TypeFast and get my HelmRelease boilerplate in one click. I used to keep these in a text file, but searching and copying from a file is slower than TypeFast’s fuzzy search + click-to-copy.

    Focus blocks: I use FocusForge for 45-minute deep work sessions when writing complex articles or debugging tricky Kubernetes issues. The gamification is silly but effective — maintaining a streak makes me less likely to check Slack mid-session. I’ve tracked my output: I write roughly 40% more words per hour during FocusForge sessions versus unstructured time.

    The meta-point: None of these tasks justify installing a desktop app. They’re all quick, one-off operations that happen multiple times a week. Browser tools eliminate the friction of “open app → wait for it to load → do the thing → close app” and replace it with “open tab → do the thing → close tab.” That 30-second difference per task adds up to hours per month.

    Building Your Own Browser Tool

    If you want to build a browser-based file processing tool, the pattern is always the same: accept a file, process it in JavaScript, and offer the result as a download. Here’s a reusable file dropper component that handles drag-and-drop:

    const dropZone = document.getElementById('drop-zone');
    
    dropZone.addEventListener('dragover', (e) => {
      e.preventDefault();
      dropZone.classList.add('drag-active');
    });
    
    dropZone.addEventListener('dragleave', () => {
      dropZone.classList.remove('drag-active');
    });
    
    dropZone.addEventListener('drop', (e) => {
      e.preventDefault();
      dropZone.classList.remove('drag-active');
      const file = e.dataTransfer.files[0];
      if (file) processFile(file);
    });
    
    async function processFile(file) {
      const buffer = await file.arrayBuffer();
      const result = transformData(buffer);
    
      const blob = new Blob([result], { type: file.type });
      const url = URL.createObjectURL(blob);
      const link = document.createElement('a');
      link.href = url;
      link.download = 'processed-' + file.name;
      link.click();
      URL.revokeObjectURL(url);
    }

    The arrayBuffer() method gives you raw binary access to the file — useful for parsing headers, manipulating pixels, or stripping metadata. The key insight is the Blob and URL.createObjectURL() pattern: this creates a temporary URL pointing to in-memory data, which you can assign to a download link. The user gets a file download without any server involvement.

    Performance Comparison: Browser vs Desktop

    Browser-based tools aren’t just more private — they’re surprisingly fast. Here’s how Canvas API compression compares to desktop tools on a typical 4000×3000 JPEG photo:

    • Canvas API (browser) — ~120ms to compress, ~800KB output at 80% quality
    • ImageMagick (desktop CLI) — ~200ms to compress, ~750KB output at 80% quality
    • Pillow/Python (scripting) — ~180ms to compress, ~770KB output at 80% quality
    • MozJPEG via WASM (browser) — ~300ms to compress, ~680KB output at 80% quality (better compression ratio)

    The browser’s Canvas encoder is faster because it uses the GPU-accelerated rendering pipeline that’s already running. ImageMagick produces slightly smaller files because it uses more sophisticated encoding algorithms — but the difference is under 10% for typical photos.

    Where browser tools break down:

    • Very large files (50MB+) — Canvas can hit memory limits, especially on mobile devices. Desktop tools handle arbitrarily large files through streaming.
    • Batch processing — compressing 500 images is painful one-by-one in a browser. A shell script with ImageMagick does it in seconds: mogrify -quality 80 *.jpg
    • Exotic formats — HEIC, RAW, TIFF support varies across browsers. Desktop tools like FFmpeg and ImageMagick support everything.
    • Video processing — while possible with WASM, it’s still orders of magnitude slower than native FFmpeg.

    The sweet spot is clear: for one-off tasks with standard formats, browser tools are faster and more private. For batch jobs, automation pipelines, and edge cases, use desktop tools. The progressive enhancement approach works well — start with a browser tool, fall back to CLI when needed.

    Deep Dives

    Want the full story behind each tool? Read our detailed write-ups: QuickShrink: Why I Built a Browser-Based Image Compressor, PixelStrip: Your Photos Are Broadcasting Your Location, and TypeFast: The Snippet Manager for People Who Refuse to Install Another App.

    Source availability and implementation notes are listed on each tool’s project page.

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    Frequently Asked Questions

    What is 5 Free Browser Tools That Replace Desktop Apps about?

    You don’t need to install an app for everything. These browser-based tools work instantly — no download, no account, no tracking.

    Who should read this article about 5 Free Browser Tools That Replace Desktop Apps?

    Anyone interested in learning about 5 Free Browser Tools That Replace Desktop Apps and related topics will find this article useful.

    What are the key takeaways from 5 Free Browser Tools That Replace Desktop Apps?

    They run entirely on your device and work offline once loaded. Image Compression → QuickShrink Instead of installing Photoshop or GIMP just to resize an image, open QuickShrink . Drop an image, pick q

    References

  • How to Remove GPS Location from Photos Before Sharing Online

    How to Remove GPS Location from Photos Before Sharing Online

    Every time you take a photo with your phone, the exact GPS coordinates are embedded in the image file. When you share that photo online — on forums, marketplaces, or messaging apps — anyone who downloads it can see exactly where you were standing. Here’s how to remove it in 3 seconds.

    Quick Fix: Strip All Metadata

    📌 TL;DR: Every time you take a photo with your phone, the exact GPS coordinates are embedded in the image file. When you share that photo online — on forums, marketplaces, or messaging apps — anyone who downloads it can see exactly where you were standing. Here’s how to remove it in 3 seconds.
    🎯 Quick Answer: Every smartphone photo embeds GPS coordinates in EXIF metadata that reveal your exact location. Remove GPS data in 3 seconds using a browser-based EXIF stripper—no uploads, no installs, no accounts required.
    🛡️ Bonus: shrink while you strip. Re-encoding a photo through your browser’s Canvas (exactly how a client-side compressor works) also wipes EXIF/GPS metadata as a side effect — and you get a smaller file for sharing. QuickShrink compresses images entirely in your browser (nothing is uploaded to any server), so the photo you share is both metadata-free and lighter. Compress a photo privately →
    1. Open PixelStrip
    2. Drop your photo on the page
    3. See the metadata report (GPS coordinates highlighted in red)
    4. Click “Strip All Metadata & Download”

    The downloaded photo looks identical but contains zero hidden data. No GPS, no camera model, no timestamps.

    What Metadata Is Actually in Your Photos?

    EXIF metadata was designed for photographers to track camera settings. But smartphones added fields that reveal far more than you’d expect:

    • GPS Latitude/Longitude — accurate to within 3 meters on modern phones
    • Device Model — “iPhone 16 Pro” or “Samsung Galaxy S25” — narrows down who took the photo
    • Date & Time — the exact second the photo was captured
    • Camera Serial Number — a unique identifier that links photos from the same device
    • Thumbnail — a smaller version that may contain content you cropped out

    Which Platforms Strip Metadata Automatically?

    Do strip metadata: Facebook, Instagram, Twitter/X, WhatsApp (when sent as photo, not file)

    Don’t strip metadata: Email, Telegram (file mode), Discord, Forums, Craigslist, eBay, Dropbox, Google Drive shared links

    If you’re sharing via any channel that doesn’t strip metadata, do it yourself first.

    👉 Strip your photos with PixelStrip — no upload, no account, 100% in your browser.

    Command-Line GPS Removal With ExifTool

    ExifTool is the Swiss Army knife of metadata manipulation. It reads and writes EXIF, IPTC, XMP, and dozens of other metadata formats across virtually every image type. For GPS removal specifically, it offers surgical precision — you can strip location data while preserving camera settings, copyright info, and other tags you want to keep.

    # View all GPS-related tags in a photo
    exiftool -gps:all photo.jpg
    
    # View GPS as a Google Maps link
    exiftool -n -p "https://maps.google.com/?q=$GPSLatitude,$GPSLongitude" photo.jpg
    
    # Remove ONLY GPS tags (keep everything else)
    exiftool -gps:all= photo.jpg
    
    # Remove GPS from all JPEGs in a folder
    exiftool -gps:all= -overwrite_original *.jpg
    
    # Remove GPS + create backup of originals
    exiftool -gps:all= -o stripped/ *.jpg
    
    # Nuclear option: remove ALL metadata
    exiftool -all= photo.jpg
    
    # Verify GPS is gone
    exiftool -gps:all photo.jpg
    # Should output: (no output = no GPS tags)

    The key flag is -gps:all= (with the equals sign and no value). This tells ExifTool to set all GPS-related tags to empty, effectively deleting them. The -overwrite_original flag skips creating backup files (ExifTool normally saves photo.jpg_original as a safety net). For batch processing, the wildcard *.jpg handles every JPEG in the current directory.

    For automated workflows, here’s a shell script that watches a folder and strips GPS from any new images:

    #!/bin/bash
    # auto_strip_gps.sh - Watch a folder and strip GPS from new images
    WATCH_DIR="${1:-.}"
    PROCESSED_LOG="$WATCH_DIR/.stripped_files"
    touch "$PROCESSED_LOG"
    
    echo "Watching $WATCH_DIR for new images..."
    
    while true; do
      find "$WATCH_DIR" -maxdepth 1 -type f \( -name "*.jpg" -o -name "*.jpeg" -o -name "*.png" \) | while read -r file; do
        if ! grep -qF "$file" "$PROCESSED_LOG" 2>/dev/null; then
          echo "Stripping GPS from: $file"
          exiftool -gps:all= -overwrite_original "$file" 2>/dev/null
          echo "$file" >> "$PROCESSED_LOG"
        fi
      done
      sleep 5
    done

    And here’s a Python wrapper that gives you more control — logging, error handling, and the ability to selectively preserve certain tags:

    import subprocess
    import os
    import json
    
    def get_gps_data(filepath):
        result = subprocess.run(
            ["exiftool", "-json", "-gps:all", filepath],
            capture_output=True, text=True
        )
        if result.returncode != 0:
            return None
        data = json.loads(result.stdout)
        return data[0] if data else None
    
    def strip_gps(filepath, keep_backup=False):
        cmd = ["exiftool", "-gps:all="]
        if not keep_backup:
            cmd.append("-overwrite_original")
        cmd.append(filepath)
        result = subprocess.run(cmd, capture_output=True, text=True)
        return result.returncode == 0
    
    def batch_strip_gps(folder, extensions=None):
        if extensions is None:
            extensions = {".jpg", ".jpeg", ".tiff", ".png"}
        results = {"stripped": [], "skipped": [], "errors": []}
        for fname in os.listdir(folder):
            ext = os.path.splitext(fname)[1].lower()
            if ext not in extensions:
                continue
            fpath = os.path.join(folder, fname)
            gps = get_gps_data(fpath)
            if gps and any(k.startswith("GPS") for k in gps):
                if strip_gps(fpath):
                    results["stripped"].append(fname)
                else:
                    results["errors"].append(fname)
            else:
                results["skipped"].append(fname)
        return results
    
    # Usage:
    # results = batch_strip_gps("/path/to/photos")
    # print(f"Stripped: {len(results['stripped'])}")
    # print(f"Skipped (no GPS): {len(results['skipped'])}")

    The Python wrapper adds intelligence that the raw command lacks. It checks whether GPS data actually exists before attempting removal (avoiding unnecessary file writes), tracks which files were processed, and separates results into stripped/skipped/error categories. This is useful when processing large photo libraries where most images may already be clean.

    Building a Browser-Based GPS Stripper

    Command-line tools are powerful but require installation. For a zero-install solution, you can build a browser-based GPS stripper that processes photos entirely client-side. The challenge: you need to parse the JPEG binary format, find the GPS IFD (Image File Directory) entries, and nullify them — all without corrupting the rest of the file.

    // Parse JPEG to find and nullify GPS data
    // GPS lives in the EXIF APP1 segment (marker 0xFFE1)
    
    async function removeGPSFromJPEG(file) {
      const buffer = await file.arrayBuffer();
      const bytes = new Uint8Array(buffer);
    
      // Verify JPEG signature
      if (bytes[0] !== 0xFF || bytes[1] !== 0xD8) {
        throw new Error("Not a valid JPEG file");
      }
    
      // Find APP1 (EXIF) segment
      let offset = 2;
      while (offset < bytes.length - 1) {
        if (bytes[offset] !== 0xFF) break;
        const marker = bytes[offset + 1];
    
        // APP1 = 0xE1
        if (marker === 0xE1) {
          const segLen = (bytes[offset+2] << 8) | bytes[offset+3];
          // Check for EXIF header: "Exif\0\0"
          const header = String.fromCharCode(
            bytes[offset+4], bytes[offset+5],
            bytes[offset+6], bytes[offset+7]
          );
          if (header === "Exif") {
            nullifyGPSInSegment(bytes, offset + 10, segLen - 8);
          }
        }
    
        // SOS marker = start of image data, stop parsing
        if (marker === 0xDA) break;
    
        // Skip to next segment
        const len = (bytes[offset+2] << 8) | bytes[offset+3];
        offset += 2 + len;
      }
    
      return new Blob([bytes], { type: "image/jpeg" });
    }
    
    function nullifyGPSInSegment(bytes, tiffStart, length) {
      // Read byte order: "II" (Intel/little-endian)
      // or "MM" (Motorola/big-endian)
      const isLittleEndian = (bytes[tiffStart] === 0x49);
    
      function readUint16(pos) {
        return isLittleEndian
          ? bytes[pos] | (bytes[pos+1] << 8)
          : (bytes[pos] << 8) | bytes[pos+1];
      }
    
      function readUint32(pos) {
        return isLittleEndian
          ? bytes[pos] | (bytes[pos+1]<<8) | (bytes[pos+2]<<16) | (bytes[pos+3]<<24)
          : (bytes[pos]<<24) | (bytes[pos+1]<<16) | (bytes[pos+2]<<8) | bytes[pos+3];
      }
    
      // Parse IFD0 to find GPS IFD pointer (tag 0x8825)
      const ifd0Offset = tiffStart + readUint32(tiffStart + 4);
      const entryCount = readUint16(ifd0Offset);
    
      for (let i = 0; i < entryCount; i++) {
        const entryPos = ifd0Offset + 2 + (i * 12);
        const tagId = readUint16(entryPos);
    
        // Tag 0x8825 = GPS IFD pointer
        if (tagId === 0x8825) {
          const gpsOffset = tiffStart + readUint32(entryPos + 8);
          const gpsEntries = readUint16(gpsOffset);
          // Zero out all GPS IFD entries
          for (let j = 0; j < gpsEntries * 12 + 2; j++) {
            if (gpsOffset + j < bytes.length) {
              bytes[gpsOffset + j] = 0;
            }
          }
          // Zero the GPS IFD pointer itself
          for (let j = 0; j < 12; j++) {
            bytes[entryPos + j] = 0;
          }
          break;
        }
      }
    }
    
    // Usage with File API:
    // const input = document.getElementById("fileInput");
    // input.addEventListener("change", async (e) => {
    //   const file = e.target.files[0];
    //   const cleanBlob = await removeGPSFromJPEG(file);
    //   const url = URL.createObjectURL(cleanBlob);
    //   const a = document.createElement("a");
    //   a.href = url;
    //   a.download = "nogps_" + file.name;
    //   a.click();
    // });

    This approach is more surgical than the Canvas re-encoding method. Instead of re-creating the entire image (which causes quality loss from re-compression), it modifies the binary directly — zeroing out only the GPS IFD entries while leaving everything else intact. The image pixels, camera settings, and file structure remain untouched. The tradeoff is complexity: you need to handle both byte orderings (Intel and Motorola), navigate the TIFF IFD chain, and correctly parse variable-length tag values.

    The nullifyGPSInSegment function locates the GPS IFD by scanning IFD0 for tag 0x8825 (the GPS Info IFD Pointer). Once found, it zeros out every byte in the GPS IFD block, including the pointer itself. This is a safe operation because EXIF parsers skip zero-valued entries, and the rest of the file structure remains valid. PixelStrip uses a more refined version of this technique that also handles XMP GPS data and IPTC location fields, which can duplicate GPS information in different metadata formats.

    My Photo Privacy Workflow

    I set up an automated pipeline: every photo I export goes through ExifTool before it hits any cloud service. The workflow is simple but effective — a folder action on my Mac watches my “Export” directory and runs exiftool -gps:all= -overwrite_original on any new JPEG or PNG that appears. By the time I drag a photo into an email, a chat app, or a forum post, the GPS data is already gone.

    The tradeoff between convenience and privacy is real. GPS-tagged photos are genuinely useful — they power the “Places” album in your photo library, enable location-based search, and add context to travel memories. I don’t strip GPS from photos that stay in my private library. The rule is: GPS stays on photos I keep, GPS gets removed from photos I share. That boundary is the entire workflow in one sentence.

    Here’s what I learned about platform-by-platform metadata handling after testing each one with a GPS-tagged test image:

    • Facebook/Instagram — Strips all EXIF on upload. Your photos are safe, but Facebook stores the GPS data server-side for their own use before removing it from the public file.
    • Twitter/X — Strips EXIF. Clean since 2014 after multiple privacy incidents.
    • WhatsApp — Strips EXIF when sent as a photo. But if you send as a “document” (file attachment), all metadata is preserved. This catches people off guard.
    • Telegram — Same as WhatsApp: photo mode strips, file mode preserves.
    • Discord — Does NOT strip EXIF. Every image uploaded to Discord retains full metadata including GPS. This is a major blind spot for the gaming community.
    • Email — No stripping whatsoever. Attachments are sent as-is.
    • Google Drive / Dropbox shared links — No stripping. The recipient downloads the original file with all metadata intact.
    • Forums (Reddit, Discourse, phpBB) — Depends on configuration. Reddit strips EXIF, but most self-hosted forums do not.
    • eBay / Craigslist / Marketplace — Inconsistent. Some strip, some don’t. Always strip before uploading to any marketplace.

    The takeaway: if you’re not sure whether a platform strips metadata, assume it doesn’t. It takes 3 seconds to run a photo through PixelStrip or ExifTool. That’s a trivial cost compared to the risk of leaking your home coordinates to a stranger on the internet. I treat GPS stripping the same way I treat locking my front door — it’s not paranoia, it’s just basic hygiene.

    Related Reading

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    Frequently Asked Questions

    What is How to Remove GPS Location from Photos Before Sharing Online about?

    Every time you take a photo with your phone, the exact GPS coordinates are embedded in the image file. When you share that photo online — on forums, marketplaces, or messaging apps — anyone who downlo

    Who should read this article about How to Remove GPS Location from Photos Before Sharing Online?

    Anyone interested in learning about How to Remove GPS Location from Photos Before Sharing Online and related topics will find this article useful.

    What are the key takeaways from How to Remove GPS Location from Photos Before Sharing Online?

    Here’s how to remove it in 3 seconds. Quick Fix: Strip All Metadata Open PixelStrip Drop your photo on the page See the metadata report (GPS coordinates highlighted in red) Click “Strip All Metadata &

    References

  • Compress Images Without Losing Quality (Free Tool)

    Compress Images Without Losing Quality (Free Tool)

    You need to send a photo by email but it’s 8MB. You need to upload a product image but the CMS has a 2MB limit. You need to speed up your website but your hero image is 4MB. The solution is always the same: compress the image. But most tools upload your photo to a random server first.

    Here’s how to compress images without uploading them anywhere, using only your browser.

    The 30-Second Method

    📌 TL;DR: You need to send a photo by email but it’s 8MB. You need to upload a product image but the CMS has a 2MB limit. You need to speed up your website but your hero image is 4MB.
    🎯 Quick Answer: Compress images up to 80% smaller without visible quality loss using browser-based compression that processes photos entirely on your device. Unlike cloud tools like TinyPNG, your images never leave your browser.
    1. Open QuickShrink
    2. Drag your image onto the page (or click to select)
    3. Adjust the quality slider — 80% gives great results for most photos
    4. Click Download

    That’s it. Your image never leaves your device. The compression happens entirely in your browser using the Canvas API — the same technology that powers web-based photo editors.

    What Quality Setting Should I Use?

    80% — Best for most photos. Reduces file size by 40-60% with no visible difference. This is what most professional websites use.

    60% — Good for thumbnails, social media, and email attachments. 70-80% smaller. You might notice slight softening if you zoom in, but it looks perfect at normal viewing size.

    40% — Maximum compression. 85-90% smaller. Visible artifacts on close inspection, but fine for previews and low-bandwidth situations.

    100% — No compression. Use this only if you need to convert PNG to JPEG without quality loss.

    Why Not Use TinyPNG or Squoosh?

    TinyPNG uploads your image to their servers. For personal photos or client work, that’s a privacy concern. Also limited to 20 free compressions per month.

    Squoosh (by Google) is excellent and also runs client-side. But it’s heavier — loads a WASM codec and has a complex UI. If you just want to shrink a photo fast, it’s overkill.

    QuickShrink is a single HTML file, loads in under 200ms, works offline, and does one thing: make your image smaller. No account, no limits, no tracking.

    👉 Try QuickShrink — free, forever, private by design.

    How Image Compression Works Under the Hood

    When you drag a photo into QuickShrink and move the quality slider, here’s what actually happens at the code level. The browser’s Canvas API does the heavy lifting, but understanding the mechanics helps you choose the right settings.

    The compression pipeline has three stages:

    1. Decode — the browser reads your JPEG/PNG file and decompresses it into raw pixel data (an array of RGBA values)
    2. Render — those pixels are drawn onto an invisible HTML5 Canvas element
    3. Re-encode — the Canvas exports the image as a new JPEG at your chosen quality level

    The quality parameter controls how aggressively the JPEG encoder discards visual information. JPEG compression works by dividing the image into 8×8 pixel blocks, applying a Discrete Cosine Transform (DCT) to each block, and then quantizing the frequency coefficients. Lower quality means more aggressive quantization — more data thrown away, smaller file, more artifacts.

    The lossy vs. lossless distinction matters here. JPEG is always lossy — every re-encode loses some data. PNG is lossless — it compresses without discarding anything, which is why PNG files are larger. When QuickShrink converts a PNG to JPEG, you get dramatic size reduction because you’re switching from lossless to lossy compression.

    Here’s the JPEG quality vs. file size curve that explains the diminishing returns:

    • 100% → 95% — saves 30-40% file size with zero visible change
    • 95% → 80% — saves another 30-40% with no visible change at normal viewing
    • 80% → 60% — saves another 20-30% with slight softening visible at 100% zoom
    • 60% → 40% — saves only 10-15% more but introduces visible blockiness

    This is why 80% is the sweet spot. You capture the steepest part of the compression curve — maximum size reduction before visible quality loss.

    Here’s a complete browser-based image compressor in about 30 lines of JavaScript:

    // Complete client-side image compressor
    document.getElementById('file-input').addEventListener('change', async (e) => {
      const file = e.target.files[0];
      if (!file) return;
    
      // Decode the image
      const img = new Image();
      const objectUrl = URL.createObjectURL(file);
      await new Promise(resolve => { img.onload = resolve; img.src = objectUrl; });
      URL.revokeObjectURL(objectUrl);
    
      // Render to canvas
      const canvas = document.createElement('canvas');
      canvas.width = img.naturalWidth;
      canvas.height = img.naturalHeight;
      canvas.getContext('2d').drawImage(img, 0, 0);
    
      // Re-encode as JPEG at 80% quality
      const quality = 0.8;
      const blob = await new Promise(resolve =>
        canvas.toBlob(resolve, 'image/jpeg', quality)
      );
    
      // Show results
      const savings = ((1 - blob.size / file.size) * 100).toFixed(1);
      console.log(`Original: ${(file.size/1024).toFixed(0)}KB`);
      console.log(`Compressed: ${(blob.size/1024).toFixed(0)}KB`);
      console.log(`Saved: ${savings}%`);
    
      // Trigger download
      const a = document.createElement('a');
      a.href = URL.createObjectURL(blob);
      a.download = 'compressed-' + file.name;
      a.click();
    });

    That’s the entire core of QuickShrink. Everything else — the drag-and-drop UI, the quality slider, the preview — is just interface around these 30 lines.

    Batch Compression Script

    Browser tools work great for one-off compressions, but if you need to process an entire folder of images, command-line scripts are faster. Here are two approaches I use regularly.

    Python script using Pillow — good for cross-platform use and when you need precise control:

    #!/usr/bin/env python3
    # Batch compress JPEG images using Pillow
    import os, sys
    from pathlib import Path
    from PIL import Image
    
    def compress_images(input_dir, output_dir, quality=80):
        Path(output_dir).mkdir(parents=True, exist_ok=True)
        total_saved = 0
    
        for filename in os.listdir(input_dir):
            if not filename.lower().endswith(('.jpg', '.jpeg', '.png')):
                continue
    
            input_path = os.path.join(input_dir, filename)
            output_path = os.path.join(output_dir, filename)
            original_size = os.path.getsize(input_path)
    
            img = Image.open(input_path)
            if img.mode == 'RGBA':
                img = img.convert('RGB')
            img.save(output_path, 'JPEG', quality=quality, optimize=True)
    
            new_size = os.path.getsize(output_path)
            saved = original_size - new_size
            total_saved += saved
            print(f"{filename}: {original_size//1024}KB -> {new_size//1024}KB")
    
        print(f"Total saved: {total_saved // 1024}KB")
    
    if __name__ == '__main__':
        compress_images(sys.argv[1], sys.argv[2],
                        int(sys.argv[3]) if len(sys.argv) > 3 else 80)

    Shell one-liner using ImageMagick — fastest option if you already have it installed:

    # Compress all JPEGs in current directory to 80% quality
    mkdir -p compressed
    for f in *.jpg *.jpeg *.png; do
      [ -f "$f" ] || continue
      convert "$f" -quality 80 -strip "compressed/$f"
      echo "$f: $(du -k "$f" | cut -f1)KB -> $(du -k "compressed/$f" | cut -f1)KB"
    done

    The -strip flag removes all metadata (EXIF, ICC profiles, etc.), which often saves an additional 20-50KB per image. If you want to keep color profiles, drop that flag.

    Comparing output sizes at different quality levels for a typical 4MB smartphone photo:

    • Quality 95 — 2.4MB (40% reduction)
    • Quality 80 — 820KB (80% reduction) ← the sweet spot
    • Quality 60 — 480KB (88% reduction)
    • Quality 40 — 320KB (92% reduction)
    • Quality 20 — 180KB (95% reduction, visible artifacts)

    The jump from 95 to 80 gives you the most bang for your buck — nearly halving the file size from the 95% baseline with no perceptible quality loss.

    Why I Built QuickShrink Instead of Using Existing Tools

    Every free image compressor I could find had the same problem: they upload your files to their server. TinyPNG, Compressor.io, iLoveIMG — they all send your images over the network, process them on their infrastructure, and send them back. That’s a privacy problem.

    Think about what you’re compressing. Product photos for your business. Screenshots of your desktop with open tabs. Family photos you want to email. Medical images for an insurance claim. None of that should pass through a third-party server just because you need to reduce file size.

    I started building QuickShrink as a weekend project after reading the privacy policies of five popular image compressors. Most said they delete uploaded files “within a few hours” — but that still means your photos sit on someone else’s server, unencrypted, for hours. Some didn’t specify a deletion timeline at all.

    The technical challenge was matching server-side compression quality using only browser APIs. Server-side tools like MozJPEG and libjpeg-turbo have decades of optimization behind them. The browser’s built-in JPEG encoder is good but not as sophisticated. After extensive testing, here’s what I found:

    • At 80% quality, the browser’s output is within 5-8% of MozJPEG for file size — close enough for practical use
    • At 60% quality, MozJPEG pulls ahead significantly — its perceptual optimization produces noticeably better results at high compression
    • Visual quality at 80% is indistinguishable between browser and server in blind tests I ran with 20 sample images

    For the 80% quality range that most people need, browser compression is good enough. And the privacy tradeoff makes it the clear winner. Your files never leave your device. There’s no server to get breached, no upload to be intercepted, no retention policy to worry about.

    QuickShrink loads in under 200ms, works offline after first visit, and is a single HTML file with zero dependencies. That’s the kind of tool the web was supposed to give us — fast, private, and free.

    Related Reading

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    Frequently Asked Questions

    How can I compress images without losing quality?

    Modern image compression tools use smart algorithms that reduce file size by removing redundant data while preserving visual quality. Techniques like lossless PNG compression and optimized JPEG encoding can reduce file sizes by 50-80% with no visible difference to the human eye.

    What is the difference between lossy and lossless image compression?

    Lossless compression reduces file size without discarding any image data, so the decompressed image is pixel-identical to the original. Lossy compression achieves smaller files by permanently removing some visual information, though at high quality settings the difference is imperceptible.

    Does compressing images affect SEO?

    Yes, image compression significantly improves page load speed, which is a direct Google ranking factor. Faster-loading pages also reduce bounce rates and improve Core Web Vitals scores, giving compressed-image pages a measurable advantage in search results.

    What image format should I use for web: JPEG, PNG, or WebP?

    Use JPEG for photographs and complex images with many colors. Use PNG for graphics, logos, and images requiring transparency. WebP offers the best of both with 25-35% smaller files than JPEG at equivalent quality, and is now supported by all major browsers.

    References

  • NoiseLog: A Sound Meter App for Noisy Neighbors

    NoiseLog: A Sound Meter App for Noisy Neighbors

    When I complained about noise to my building manager, they asked for evidence. “It’s loud” wasn’t enough. They wanted dates, times, and decibel readings. So I built an app that gives you all three — and generates a report you can actually hand to someone.

    The Noise Complaint Trap

    📌 TL;DR: When I complained about noise to my building manager, they asked for evidence. “It’s loud” wasn’t enough. They wanted dates, times, and decibel readings.
    🎯 Quick Answer: NoiseLog is a sound meter app that records decibel readings with timestamps and automatically generates formatted noise complaint reports with date, time, duration, and dB levels—ready to submit to landlords or local authorities.

    Here’s how noise complaints typically go: you’re frustrated, you call the landlord or the city, they say “we’ll look into it,” nothing happens. Why? Because verbal complaints carry almost zero weight. Without documentation — specific dates, times, duration, and measured intensity — you’re just another person saying “it’s too loud.”

    Professional sound level meters cost $200+. An acoustic engineering assessment starts at $500. Most people just suffer in silence (pun intended) or escalate to a confrontation. Neither is a great outcome.

    Your Phone Microphone Is Good Enough

    Modern smartphone microphones are surprisingly capable. They won’t match a calibrated Type I sound meter, but for documenting noise levels in the 40–100 dB range — which covers everything from a loud TV to construction equipment — they’re more than adequate. Courts and housing authorities don’t require laboratory-grade measurements; they require consistent, timestamped records.

    NoiseLog uses your phone’s microphone to capture ambient sound, processes it through a standard A-weighted decibel calculation, and displays the result in real time.

    Three Screens, One Workflow

    Sound Meter. A live dB reading with a 60-second rolling chart. Color bands show you whether the noise level is safe (green), moderate (yellow), loud (orange), or harmful (red, 85+ dB). The day limit indicator shows if you’ve been exposed to noise above safe thresholds.

    Incidents. One tap logs the current noise level with a timestamp. “Tuesday, 11:47 PM, 78 dB.” Over a few days or weeks, you build a pattern. “This happens every weeknight between 11 PM and 2 AM, averaging 72 dB.” That’s not a complaint — that’s evidence.

    Report. Generate a formatted summary of all logged incidents. Dates, times, decibel readings, in a clean layout you can screenshot, print, or share. Hand it to your landlord, attach it to a noise ordinance complaint, or bring it to a mediator. Structured data is harder to ignore than “my neighbor is loud.”

    Beyond Neighbors

    Noise complaints are the obvious use case, but people have found others:

    • Workplace safety — OSHA requires hearing protection above 85 dB. NoiseLog helps document whether your factory floor or machine shop meets standards
    • Event planning — check if your venue stays within local noise ordinance limits during rehearsals
    • Parenting — curiosity check: how loud is that toy your kid loves? (Often shockingly loud)
    • Musicians — monitor rehearsal room levels to protect hearing
    • Real estate — measure ambient noise levels in a potential apartment before signing a lease

    Free vs. Pro

    The free version is fully functional — real-time metering, incident logging, reports. The only friction is a short video ad when you start a measurement session. The Pro subscription ($1.99/month) removes ads, unlocks unlimited incident storage, and enables detailed CSV export for anyone who needs to submit formal documentation.

    How Decibel Measurement Works in the Browser

    NoiseLog’s core measurement engine runs entirely in the browser using the Web Audio API. No server, no uploads, no third-party SDKs touching your microphone data. Here’s the signal chain that turns raw microphone input into a decibel reading:

    // Web Audio API: microphone to dB meter
    async function initMeter() {
      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
      const audioCtx = new AudioContext();
      const source = audioCtx.createMediaStreamSource(stream);
      const analyser = audioCtx.createAnalyser();
      analyser.fftSize = 2048;
      source.connect(analyser);
    
      const buffer = new Float32Array(analyser.fftSize);
    
      function calculateDB() {
        analyser.getFloatTimeDomainData(buffer);
    
        // RMS (Root Mean Square) calculation
        let sumSquares = 0;
        for (let i = 0; i < buffer.length; i++) {
          sumSquares += buffer[i] * buffer[i];
        }
        const rms = Math.sqrt(sumSquares / buffer.length);
    
        // Convert RMS to decibels
        const db = 20 * Math.log10(Math.max(rms, 1e-10));
    
        // A-weighting approximation for human-perceived loudness
        // Boosts mid-frequencies (1-6 kHz) where human hearing is most sensitive
        const aWeighted = applyAWeighting(db, analyser, buffer);
    
        return {
          raw: db.toFixed(1),
          aWeighted: aWeighted.toFixed(1),
          rms: rms
        };
      }
    
      // A-weighting filter approximation
      function applyAWeighting(dbValue, analyser, timeDomain) {
        const freqData = new Float32Array(analyser.frequencyBinCount);
        analyser.getFloatFrequencyData(freqData);
        const nyquist = audioCtx.sampleRate / 2;
        let weightedSum = 0;
        let totalEnergy = 0;
    
        for (let i = 0; i < freqData.length; i++) {
          const freq = (i / freqData.length) * nyquist;
          const energy = Math.pow(10, freqData[i] / 10);
          // Simplified A-weighting curve
          const weight = aWeightCoeff(freq);
          weightedSum += energy * weight;
          totalEnergy += energy;
        }
    
        const correction = totalEnergy > 0
          ? 10 * Math.log10(weightedSum / totalEnergy)
          : 0;
        return dbValue + correction;
      }
    
      function aWeightCoeff(f) {
        // IEC 61672 A-weighting approximation
        const f2 = f * f;
        const num = 1.4866e8 * f2 * f2;
        const den = (f2 + 424.36) * Math.sqrt((f2 + 11599.29) *
          (f2 + 544496.41)) * (f2 + 148693636);
        const ra = num / den;
        return ra * ra * 1.9997;
      }
    
      // Update loop at 30fps
      setInterval(() => {
        const reading = calculateDB();
        updateDisplay(reading);
        logToHistory(reading);
      }, 33);
    }

    The key steps: getUserMedia grabs the microphone stream, the AnalyserNode provides raw audio samples, and then it’s just math. RMS gives you the average energy in the signal, and the log10 conversion maps it to the decibel scale humans are used to. The A-weighting filter is critical — without it, you’d measure low-frequency rumble (like HVAC systems) at the same level as a conversation, which isn’t how humans perceive loudness.

    Calibration and Accuracy

    Let’s be honest: your phone microphone is not a professional SPL meter. It has a limited dynamic range, nonlinear frequency response, and automatic gain control that most browsers can’t fully disable. But here’s the thing — for the purpose of documenting noise complaints, it doesn’t need to be perfect. It needs to be consistent and timestamped.

    I calibrated NoiseLog against a UNI-T UT353 mini sound level meter (about $30 on most electronics sites) in my apartment. The process was straightforward: play pink noise at a known level, read both meters simultaneously, and calculate the offset. Here’s what I found across different volume levels:

    • 40–50 dB (quiet room): Phone read 2–4 dB lower than the SPL meter. Microphone sensitivity drops off at very low levels.
    • 50–70 dB (conversation to loud TV): Within ±2 dB. This is the sweet spot where phone microphones are most accurate.
    • 70–85 dB (loud music, vacuum cleaner): Within ±3 dB. Still reliable enough for evidence.
    • 85+ dB (harmful levels): Phone microphone begins to clip. Readings plateau around 90–95 dB regardless of actual level. If you’re measuring construction noise, the phone underreports.

    For the 50–85 dB range — which covers virtually every noise complaint scenario (loud neighbors, barking dogs, late-night parties) — a typical smartphone accuracy of ±3 dB is more than adequate. Housing authorities and mediators aren’t looking for laboratory precision. They’re looking for a pattern: repeated measurements at specific times showing levels above the local ordinance threshold.

    NoiseLog includes a calibration offset setting so you can adjust readings if you do have access to a reference meter. The code is simple:

    // Calibration offset stored in user settings
    const CALIBRATION_KEY = 'noiselog_calibration';
    
    function getCalibratedDB(rawDB) {
      const offset = parseFloat(
        localStorage.getItem(CALIBRATION_KEY) || '0'
      );
      return rawDB + offset;
    }
    
    // User measures a known source, enters the reference value
    function calibrate(measuredDB, referenceDB) {
      const offset = referenceDB - measuredDB;
      localStorage.setItem(CALIBRATION_KEY, offset.toString());
      console.log(
        'Calibration offset set to ' + offset.toFixed(1) + ' dB'
      );
    }

    One calibration session with a known reference point improves accuracy significantly. Even without calibration, the relative measurements are consistent — if NoiseLog says tonight is 8 dB louder than last night, that’s a real difference regardless of the absolute number.

    From Side Project to Noise Complaint Evidence

    My upstairs neighbor’s subwoofer was the reason this app exists. Every night between 11 PM and 1 AM, bass-heavy music would vibrate through the ceiling. I asked politely. I left a note. I called the building manager, who asked: “Can you document the dates and times? Do you have any readings?”

    I didn’t. So I built NoiseLog over a weekend, and for the next three weeks, I logged every incident. Tap the button when it starts, tap again when it stops. The app records the timestamp, peak dB, average dB, and duration automatically.

    After 21 days, I generated a report showing 18 incidents, all between 10:45 PM and 1:30 AM, averaging 68 dB with peaks hitting 76 dB. For context, most local noise ordinances set the nighttime limit at 50–55 dB for residential areas. My data showed consistent, documented violations — not a one-time complaint.

    The building manager took it seriously because structured data is harder to dismiss than “my neighbor is loud.” They issued a formal warning, and the late-night subwoofer sessions stopped within a week. No confrontation, no escalation, just data.

    What I learned about noise ordinances along the way: they vary wildly by municipality, but almost all of them distinguish between daytime and nighttime limits, and most set the residential nighttime threshold between 45 and 55 dB measured at the property line. Some jurisdictions use C-weighting (which captures bass better than A-weighting), and some require measurements from inside the affected unit. Check your local code before relying on any specific number.

    The most important lesson: time-series logging matters more than peak readings. A single 80 dB spike could be a dropped pan — it means nothing. But 18 data points showing sustained 65+ dB levels between 11 PM and 1 AM over three weeks? That’s a pattern, and patterns are what enforcement agencies act on. That’s why NoiseLog emphasizes incident logging over instantaneous measurements — the real value is in the history, not the number on screen right now.

    More From the App Factory

    NoiseLog is part of our collection of free browser and mobile tools built to solve real problems. If you like the idea of gamified productivity, check out FocusForge — a Pomodoro timer with XP, levels, and daily streaks. We also wrote about why the Pomodoro Technique actually works when your timer has streaks.

    Get It

    👉 NoiseLog on Google Play (Android)

    If you’re dealing with noise issues, start logging today. A week of data is worth more than a year of verbal complaints.

    Get Weekly Security & DevOps Insights

    Join 500+ engineers getting actionable tutorials on Kubernetes security, homelab builds, and trading automation. No spam, unsubscribe anytime.

    Subscribe Free →

    Delivered every Tuesday. Read by engineers at Google, AWS, and startups.

    Frequently Asked Questions

    What is NoiseLog and how does it measure sound?

    NoiseLog is a sound meter app that uses your device’s microphone to measure ambient noise levels in decibels. It provides real-time readings and logs noise events over time, creating a documented record useful for noise complaints or monitoring environmental sound levels.

    Can I use a sound meter app as evidence for noise complaints?

    A sound meter app provides supporting documentation for noise complaints by logging decibel levels with timestamps. While phone-based measurements may not be legally calibrated, a consistent log of excessive noise readings strengthens your case when reporting to landlords or local authorities.

    How loud is too loud for residential noise?

    Most local ordinances set residential noise limits between 55-65 decibels during the day and 45-55 decibels at night. Sustained noise above 70 decibels can cause hearing damage over time, and sounds above 85 decibels are considered hazardous with prolonged exposure.

    How accurate are phone-based sound meter apps?

    Modern smartphones can measure noise levels within 2-5 decibels of professional equipment for typical indoor ranges (40-90 dB). Accuracy varies by device and microphone quality, but for documenting noise patterns and relative changes, phone-based meters are sufficiently reliable.

    References

Also by us: StartCaaS — AI Company OS · Hype2You — AI Tech Trends