ADFA-5552: Optimize every media file in documentation.db (in place) - #33
ADFA-5552: Optimize every media file in documentation.db (in place)#33alexmmiller wants to merge 3 commits into
Conversation
A single, self-contained script that shrinks every image stored in a documentation.db in place - same Content.path, extension and contentTypeID, so every page reference keeps resolving. Reuses the optimization approach from the Kotlin website pipeline (pngquant + 500px downscale for PNG, JPEG re-encode, animated-GIF-aware resize, WebP re-encode, Scour for SVG) and applies it across all doc sets. Optimizes in place rather than converting formats: non-Kotlin pages reference media by absolute URL with no literal link to the stored path, so renaming (e.g. to WebP) can't be rewritten safely - keeping path and extension fixed avoids the problem entirely. Handles both plain-Brotli and shared-dictionary (CompressionDictionary) databases, reassembles chunked (>1MB) media before optimizing and re-chunks on write, and only ever rewrites a row when the result is smaller. Backs up first, runs in one transaction, VACUUMs, and supports --dry-run. Co-Authored-By: Claude Opus 4.8 <[email protected]>
0d43357 to
a4e5516
Compare
Declare the Python dependencies (Pillow, scour, brotli) inline with PEP 723 script metadata so `uv run scripts/optimize_db_media.py <db>` installs them on the fly - no requirements.txt or --with flags needed. Add `from __future__ import annotations` so the `X | None` hints run on Python 3.9+, and update the usage docstring. (pngquant and brotli remain system CLIs the user installs.) Co-Authored-By: Claude Opus 4.8 <[email protected]>
optimize_db_media.py gains a --webp mode that converts stored media to WEBP
under a new path/extension and deletes the old row, alongside the existing
in-place mode. update_media_references.py then repairs the links that
conversion leaves dangling, across every doc set.
The reference updater matches on filename rather than stored path: conversion
changes only the extension, and each doc set writes references differently
(Android "media/static_..._foo.gif", IntelliJ "./images/foo.gif", Kotlin a
JSON-escaped "/k/html/images/..."), with no single form that maps onto
Content.path. Filenames are unambiguous here - no two images share a basename
within a doc set, and no converted name collides with one left alone. The
rename map is a diff against the pre-conversion copy (--before) rather than
inferred from the converted database: inferring "every stored X.webp implies a
converted X.png" produced 4530 candidate names where only 745 files had really
converted, each spurious entry rewriting links to unrelated external assets.
Fixes from the /code-review pass:
- A file was renamed whenever its extension differed from its type's
canonical one, so "photo.jpeg" became "photo.jpg" even in the default mode
documented as never renaming. optimize_media now returns the content type
it produced, so only a real format change renames anything.
- Inserting a "<path>-N" chunk row could hit an unrelated row of that name
(content_chunking notes those are legal as separate pages) and abort the
whole transaction on UNIQUE. clear_fragment_slots frees the slots first,
logging what it removes.
- target_path split the extension off the whole path, so a dotted directory
with an extensionless file ("a/v1.2/logo") targeted "a/v1.webp".
- Writes hardcoded templateId 0, against populate_db's convention that
fragments reuse the base row's languageID/contentTypeID/templateId.
- --before is validated as the same database lineage before anything is
rewritten; a mistyped backup no longer silently rewrites 29k rows.
- Reference matching is anchored to a URL-ish delimiter, so a filename in
prose ("save it as copy.png") is no longer rewritten.
- Images were decoded twice on the default path; the open image is now
handed to the encoders.
- Animated WEBP/APNG skips are counted separately from unhandled types,
backup names take the next free suffix instead of failing on a same-second
rerun, and flags that do nothing under --webp now warn.
Measured on a 249MB database: 745 files converted, image bytes 130.2MB ->
60.6MB (-53.5%), whole file 249.1MB -> 178.4MB (-28.4%), then 1699 references
rewritten across 578 rows. integrity_check/foreign_key_check pass, all 1192
images decode, no row links to a deleted filename, and templateId is preserved
(3507 non-zero, unchanged).
Co-Authored-By: Claude Opus 4.8 <[email protected]>
Update: WebP conversion + reference updating, and a review passThis PR now carries two scripts. What changed
Why it matches on filenameConversion changes only the extension, and each doc set writes references differently — Android Worth recording: Android's The rename map is a diff, not an inference
Review fixes (
|
| Before | After | |
|---|---|---|
| Database file | 249.1 MB | 178.4 MB (−28.4%) |
| Optimizable image bytes | 130.2 MB | 60.6 MB (−53.5%) |
| image/png | 733 rows, 69.31 MB | 20 rows, 0.02 MB |
| image/jpeg | 13 rows, 0.28 MB | 0 |
| image/gif | 139 rows, 55.27 MB | 114 rows, 49.53 MB |
| image/webp | 161 rows, 2.18 MB | 906 rows, 8.14 MB |
| image/svg+xml | 183 rows, 3.20 MB | 183 rows, 2.90 MB |
| video (mp4/quicktime) | 16.61 MB | 16.61 MB (untouched) |
745 files converted; 1699 references rewritten across 578 rows.
Verification: integrity_check and foreign_key_check pass; all 1192 images decode with 0 extension/format mismatches; 0 rows still link to a deleted filename; templateId preserved exactly (3507 non-zero, unchanged); the reference pass is idempotent (a second run finds 0). The conversion output was byte-identical to the pre-fix run — the fixes close latent holes this database doesn't happen to contain — and clear_fragment_slots reported 0 collisions. Tightening the reference anchor produced the identical 1699 references, so removing the prose risk cost nothing.
Notes
- Animated GIFs are now ~82% of remaining media (49.5 MB), untouched by design. Biggest remaining opportunity.
- 5 large SVGs fall back to minified-SVG instead of rasterizing when native
cairoisn't installed (graceful, logged). ~2.9 MB of SVG remains, mostly one file. - Both scripts declare their dependencies inline (PEP 723), so
uv run scripts/<name>.py …works with no setup.pngquantandbrotliare system CLIs — under--webppngquant isn't needed at all.
|
|
||
| # Content types whose stored text can carry a link to a media file. | ||
| TEXT_TYPES = ("text/html", "application/json", "text/css", "text/markdown", | ||
| "text/javascript", "text/plain", "application/xml") |
There was a problem hiding this comment.
Why is text/plain relevant?
Documentation Database — Media Optimization Report
Ticket: ADFA-5552 · Branch:
fix/ADFA-5552Summary
Every image in
documentation.dbwas 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.Integrity verified afterward:
PRAGMA integrity_checkandforeign_key_checkboth pass, and all 1,192 image records reassemble and decode correctly.Scope
Optimization was applied to every media file in the database, across all documentation sets (Android
a/, IntelliJi/, Kotlink/,p/, Javaj/) — not just the Kotlin website. It reuses the optimization pipeline from PR #24 (the Kotlin website'soptimize_media.py) and applies it to the images already stored in the database.Videos (
mp4,quicktime) and favicons (x-icon) were intentionally left untouched — the pipeline does not re-encode video.Savings by media type
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).
Methods — exact settings
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:
--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.Key design decision: optimize in place, no format changes
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.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.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.
Correctness and safety properties
documentation.db.backup-YYYYMMDD-HHMMSS) before any change.VACUUMed to reclaim freed space. A--dry-runmode does all the work and reports savings without writing anything.CompressionDictionarytable). The tool detects which and, for dictionary databases, round-trips those rows through thebrotliCLI using the dictionary read from the database itself. The decompress→optimize→recompress→decompress round-trip was confirmed lossless before any write.Reproducing / running it
Add
--dry-runto preview savings without modifying the database. Other tunables:--jpeg-quality,--webp-quality,--pngquant-speed,--svg-precision,--verbose. Requires thepngquantandbrotlicommand-line tools onPATH.Code: a single self-contained file —
scripts/optimize_db_media.py. The database chunking protocol is inlined into it (rather than imported), so this PR adds exactly one file tomainwith no other in-repo dependencies.A note on image width (worth a group decision)
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.