|
| 1 | +# Documentation Database — Media Optimization Report |
| 2 | + |
| 3 | +**Ticket:** ADFA-5552 · **Branch:** `fix/ADFA-5552` |
| 4 | + |
| 5 | +## Summary |
| 6 | + |
| 7 | +Every image in `documentation.db` was optimized in place, reducing the database file by **26%** with no change to how any page references its media and no loss of any image. |
| 8 | + |
| 9 | +| Metric | Before | After | Reduction | |
| 10 | +|---|---:|---:|---:| |
| 11 | +| **Database file** | 249.1 MB | 183.6 MB | **−65.5 MB (−26.3%)** | |
| 12 | +| Optimizable image bytes | 130.2 MB | 65.7 MB | −64.5 MB (−49.5%) | |
| 13 | +| Images optimized | — | 1,081 | — | |
| 14 | + |
| 15 | +Integrity verified afterward: `PRAGMA integrity_check` and `foreign_key_check` both pass, and all 1,192 image records reassemble and decode correctly. |
| 16 | + |
| 17 | +## Scope |
| 18 | + |
| 19 | +Optimization was applied to **every media file in the database**, across all documentation sets (Android `a/`, IntelliJ `i/`, Kotlin `k/`, `p/`, Java `j/`) — not just the Kotlin website. It reuses the optimization pipeline from PR #24 (the Kotlin website's `optimize_media.py`) and applies it to the images already stored in the database. |
| 20 | + |
| 21 | +Videos (`mp4`, `quicktime`) and favicons (`x-icon`) were intentionally left untouched — the pipeline does not re-encode video. |
| 22 | + |
| 23 | +## Savings by media type |
| 24 | + |
| 25 | +| Type | Images optimized | Bytes saved | Method | |
| 26 | +|---|---:|---:|---| |
| 27 | +| PNG | 720 | 57.84 MB | pngquant + downscale | |
| 28 | +| GIF | 61 | 5.70 MB | per-frame downscale (animation preserved) | |
| 29 | +| WebP | 161 | 0.58 MB | re-encode + downscale | |
| 30 | +| SVG | 126 | 0.30 MB | Scour minification | |
| 31 | +| JPEG | 13 | 0.09 MB | re-encode + downscale | |
| 32 | +| **Total** | **1,081** | **64.51 MB** | | |
| 33 | + |
| 34 | +PNG downscaling is essentially the entire win. A further 109 images were already minimal and left as-is, and 14 were left untouched because re-encoding them would have *increased* their size (see "never enlarge" below). |
| 35 | + |
| 36 | +## Methods — exact settings |
| 37 | + |
| 38 | +All raster images are downscaled with a Lanczos filter to a **maximum width of 500px**, preserving aspect ratio, and **never upscaled** (an image already ≤500px wide keeps its dimensions). Then, per format: |
| 39 | + |
| 40 | +- **PNG** — quantized with **pngquant** (`--quality 65-95`, `--speed 4`, metadata stripped). pngquant is run at full resolution first (so its palette selection sees the original color detail), then the image is downscaled, then quantized again at the delivered size. |
| 41 | +- **JPEG** — re-encoded at **quality 82**, progressive, with optimized Huffman tables. |
| 42 | +- **GIF** — every frame downscaled individually; frame count, per-frame durations, and loop count are preserved so animations keep playing. Static GIFs are simply resized. |
| 43 | +- **WebP** — re-encoded at **quality 80**, method 6 (maximum compression effort). Animated WebP is left untouched. |
| 44 | +- **SVG** — minified with **Scour**: metadata/comments/editor cruft stripped, IDs shortened, styles converted to attributes, groups collapsed, and numbers rounded to **4 decimal places**. SVGs are never rasterized to PNG (that would rename the file — see below). |
| 45 | + |
| 46 | +## Key design decision: optimize *in place*, no format changes |
| 47 | + |
| 48 | +The Kotlin website pipeline (PR #24) converts images to WebP, which **renames** files (e.g. `foo.png` → `foo.webp`), and then rewrites the image references embedded in Kotlin's stored pages. That reference-rewriting step only understands Kotlin's page format. |
| 49 | + |
| 50 | +The other documentation sets don't share that format. For example, an Android page references its media by **absolute URL** (`https://developer.android.com/images/…`), which has no literal link to the path the file is stored under (`a/devsite/media/…`) that a text substitution could follow. **Renaming media outside Kotlin therefore can't be done safely by this tooling.** |
| 51 | + |
| 52 | +The solution is to optimize each image **in place** — same path, same file extension, same content type — so every reference, however it's written, keeps resolving to the same, now-smaller file. This is why the process never converts to WebP and never rasterizes an SVG. It trades a little additional savings (WebP would shrink things further) for correctness and uniform coverage across all doc sets. |
| 53 | + |
| 54 | +## Correctness and safety properties |
| 55 | + |
| 56 | +- **Never enlarges an image.** A stored image is only replaced when the optimized result is actually smaller. Images that would grow on re-encode (several already-small animated GIFs) are left exactly as they were. |
| 57 | +- **Backup first.** The database is copied to a timestamped file (`documentation.db.backup-YYYYMMDD-HHMMSS`) before any change. |
| 58 | +- **Single transaction.** All updates happen in one transaction and are rolled back on any error; the file is then `VACUUM`ed to reclaim freed space. A `--dry-run` mode does all the work and reports savings without writing anything. |
| 59 | +- **Handles chunked media.** Large files are stored split across multiple 1 MB database rows. These are reassembled into the whole image before optimizing and re-split on write, agreeing exactly with how the server reads them back. (Verified: every reassembled image — including 39 continuation rows folded into their bases — decodes correctly.) |
| 60 | +- **Handles both database generations.** Older databases store SVG/WebP as plain Brotli; newer ones compress them against a shared 256 KB Brotli dictionary (from the `CompressionDictionary` table). The tool detects which and, for dictionary databases, round-trips those rows through the `brotli` CLI using the dictionary read from the database itself. The decompress→optimize→recompress→decompress round-trip was confirmed lossless before any write. |
| 61 | + |
| 62 | +## Reproducing / running it |
| 63 | + |
| 64 | +```bash |
| 65 | +uv run --with-requirements requirements.txt scripts/optimize_db_media.py <path-to>.db --max-width 500 |
| 66 | +``` |
| 67 | + |
| 68 | +Add `--dry-run` to preview savings without modifying the database. Other tunables: `--jpeg-quality`, `--webp-quality`, `--pngquant-speed`, `--svg-precision`, `--verbose`. Requires the `pngquant` and `brotli` command-line tools on `PATH`. |
| 69 | + |
| 70 | +Code: branch **`fix/ADFA-5552`** — `scripts/optimize_db_media.py` (the optimizer) and `scripts/content_chunking.py` (the chunking protocol, shared with PR #24). |
| 71 | + |
| 72 | +## A note on image width (worth a group decision) |
| 73 | + |
| 74 | +A 500px max width was used uniformly, matching the Kotlin website's setting. For Android technical diagrams and screenshots this is fairly aggressive — most were originally wider. If preserving more detail in specific doc sets matters, the process can be re-run from the backup with a larger `--max-width` (globally or per doc set) at the cost of some of the savings above. |
0 commit comments