Skip to content

Commit 62ddc99

Browse files
alexmmillerclaude
andcommitted
ADFA-5552: Add media optimization space-savings report
Shareable writeup of what was optimized, the exact per-format settings, the in-place (no-rename) design decision, safety properties, and how to reproduce. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e5364ed commit 62ddc99

1 file changed

Lines changed: 74 additions & 0 deletions

File tree

‎MEDIA_OPTIMIZATION_REPORT.md‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
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

Comments
 (0)