/assets-optimization — AltTab asset audit and optimization
Goal
Every byte that ships in AltTab.app/Contents/Resources/ should justify itself. Vectors stay vector, rasters compress to HEIC, and neither carries metadata, color profiles, accessibility tags, or producer signatures that AppKit doesn't use.
This skill applies a known-good pipeline to each asset class. It is opinionated about the right format and the right encoder for each kind of content.
When to use
- A designer drops new exports into
~/Desktop/orresources/. - Someone asks "why is the bundle so big?".
- After adding a new icon, illustration, app icon variant, or menubar variant.
- Periodic audit when nothing else is broken.
Step 1: Inventory
Run a one-shot enumeration so you know what you're working with:
find resources -type f \( -iname '*.pdf' -o -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.heic' -o -iname '*.svg' -o -iname '*.icns' \) \
-exec ls -la {} \; | awk '{printf "%8d %s\n", $5, $9}' | sort -k2
Group what you see by directory. For AltTab the relevant buckets are:
resources/icons/menubar/— small template icons shown in the macOS menubar.resources/icons/tabs/— Preferences sidebar icons (template, sized ~13pt).resources/icons/permission-window/— first-launch permission window icons (~32pt).resources/icons/app/— the macOS app icon (.icns+.iconset/). Don't touch —.icnsis required by the bundle and Apple's tooling produces near-optimal output already.resources/illustrations/— the appearance-tab preview thumbnails. Raster (screenshots inside).
For each asset, decide what category it falls into:
| Source content | Right format | Why |
|---|---|---|
| Custom vector design (Figma/Sketch/Illustrator) | macOS 10.13 doesn't accept SVG; PDF is the universal vector container AppKit reads natively. | |
| SF Symbol (Apple system glyph) | Font glyph in the bundled SF-Pro-Text-Regular.otf subset |
Render via NSImage.fromSymbol(.foo, pointSize:) (or as text via the Symbols enum). Smaller than per-icon PDFs, picks up Apple's latest glyph refinements automatically when the developer updates SF Symbols.app. |
| Photographic / screenshot-heavy | HEIC | HEIC at q50 beats JPEG by ~30% at the same perceptual quality. |
| Tiny pixel-precise UI sprite | PNG @2x | Below ~40×40px the PDF overhead exceeds the bitmap savings. PNG wins. |
| App icon (the macOS bundle one) | .icns |
Required by CFBundleIconFile. |
If an asset is in the wrong format, flag it. If it's in the right format but unoptimized, run the matching pipeline below.
Step 2: Vector PDFs — Figma exports
Figma's "Export → PDF" output is bloated. For each menubar/illustration/icon vector PDF that came from Figma, you can strip ~50–75% of the bytes without losing a single rendered pixel.
What Figma adds that AppKit doesn't need:
- Embedded ICC color profile (
/ICCBased ..., ~3.2 KB compressed). Replace every[/ICCBased N R]reference with/DeviceRGB(or/DeviceGrayfor monochrome). Patches needed in:- the page
Resources/ColorSpacedict - every Form XObject's
Resources/ColorSpacedict (these are streams, not plain dicts — pikepdf'spdf.objectswill only catch them if you accept bothDictionaryandStream) - every Image XObject's direct
/ColorSpacekey - every Shading dict inside
Patternentries (Figma's color icons use 8+ patterns, each with its own/ColorSpace N Rreference)
- the page
/MetadataXMP packet (~830 B) — Figma's XML manifest./StructTreeRoot,/ParentTree,/StructElem— accessibility tags ("Document" / "Part" structural roles). AppKit's PDF renderer ignores them./Infodict —Producer="Figma",Title="Menubar 22x22@1x white". In Figma's exports the Info dict sometimes lives inline inside the Catalog rather than at the trailer level, so deletingpdf.docinfoisn't enough — alsodel root[Name('/Info')]./Lang,/MarkInfo,/Annots,/StructParents,/Tabs— empty or trivial page-level entries./ProcSet [/PDF]— deprecated since PDF 1.4.
Use the script:
python3 scripts/assets/optimize_figma_pdf.py resources/icons/menubar/*.pdf
It edits in place and prints the savings per file. After running, also pipe through mutool clean -ggg -z and qpdf --object-streams=generate --recompress-flate --compression-level=9 for the final 1–2% squeeze.
Verify each file still renders by sips'ing it back to PNG and eyeballing:
for f in resources/icons/menubar/*.pdf; do
sips -s format png "$f" --out "/tmp/$(basename $f .pdf).png" -Z 300 >/dev/null 2>&1
done
Open the PNGs in Preview to confirm nothing visual changed.
Step 3: SF Symbols via font subset
Every SF Symbol shipped in AltTab — switcher status icons, sidebar tab icons, button icons, permission/feedback icons — is rendered as a text glyph from a subsetted SF Pro Text font. There are no SF-Symbol PDFs in the bundle.
How it works: SF Symbols are glyphs in the Private Use Area of SF Pro Text. Apple's SF-Pro-Text-Regular.otf contains every symbol they've ever shipped. We subset it down to just the codepoints AltTab needs (currently ~36 glyphs, ~17 KB) into resources/SF-Pro-Text-Regular.otf, and register it via Info.plist:ATSApplicationFontsPath = "". At runtime, NSFont(name: "SF Pro Text", size:) resolves to the bundled subset on macOS <11 (where the system font isn't installed) and to the system font on macOS 11+, with identical glyph appearance either way.
To add a new SF Symbol:
- Open SF Symbols.app, search for the symbol, press Cmd-C to copy the symbol character to the clipboard. (Apple's name→codepoint mapping is not exposed via public API, and the SF Pro Text font's cmap uses
uniXXXXXX.medium-style names rather than semantic ones, so this manual lookup is the authoritative path.) - Paste the character into a new case on the
Symbolsenum in src/switcher/main-window/TileFontIconView.swift — e.g.,case foo = "" // SF Symbol name. - Paste the same character at the end of the
--text=argument in scripts/assets/subset_font.sh. - Run
bash scripts/assets/subset_font.sh. It reads/Library/Fonts/SF-Pro-Text-Regular.otf(installed by SF Symbols.app — a standard developer prerequisite) and writes the regenerated subset toresources/SF-Pro-Text-Regular.otf. Picks up Apple's latest glyph refinements automatically. - Use it in code:
NSImage.fromSymbol(.foo, pointSize: 14)returns a templateNSImage; orTileFontIconView(symbol: .foo, ...)for the cached-attributed-string path in the switcher hot loop.
The script runs pyftsubset via the project's pipenv environment. Warnings about MERG/meta/trak tables being dropped are normal — those tables aren't relevant to glyph rendering.
Historical note: SF Symbols via PDF (deprecated, scripts removed)
A previous pipeline shipped each SF Symbol as a per-glyph PDF in resources/icons/. The pipeline lives only in git history now (scripts/assets/export_sf_symbol_pdf.swift was deleted alongside the migrated PDFs). The technique is worth knowing in case a future need arises (e.g., a multi-color symbol that fonts can't represent):
- The naive route —
NSImage(systemSymbolName:).draw(in:)against a PDFCGContext— produces a black rectangle, because Quartz emitsimage-mask + fill-rectangleoperators where the rectangle paints over the mask. AppKit bug at the PDF emission level;paletteColorsconfig does not fix it. - The working route was to extract the symbol's vector path directly via private selectors that have been stable across macOS 11–15:
NSImage(systemSymbolName:).representations[0] (NSSymbolImageRep) → .perform("vectorGlyph") (CUINamedVectorGlyph) → .perform("CGPath") (real CGPath). - The CGPath lives in CUI's internal coordinate space (~2× display points, Y-down) — scale to fit the canvas and flip Y. Walk the path via
CGPath.applyWithBlockand emit raw PDF operators (m,l,c,h,f). Non-zero winding fill. Fill with DeviceGray (0 g), notNSColor.black.cgColor, which drags in a ~3.4 KB ICC color profile. - Final PDF wrapper: 4 objects (Catalog, Pages, Page, Content), no
/Info, no/Metadata, no/Resources/ColorSpace. ~750–1800 bytes per icon.
Git: see commit 990c1e79 ("feat: pro improve assets") for the PDF pipeline as-it-was; subsequent commit migrated the SF-Symbol PDFs back to font glyphs.
Step 4: Raster → HEIC at q50
For anything raster (illustration thumbnails, screenshots inside an icon, anything photographic), HEIC at quality 50 is the baseline. q50 is roughly 50% smaller than JPEG at perceptually-equivalent quality, and at the small display sizes used in this app the artifacts are invisible.
Pipeline (built-in to macOS via sips):
sips -Z 1000 -s format heic -s formatOptions 50 input.png --out output.heic
-Z 1000resizes the longest edge to 1000px preserving aspect ratio. AltTab's illustration display is 500pt wide, so 1000px is the correct @2x ship size. Anything larger wastes bytes; anything smaller looks soft on Retina.formatOptions 50is the quality. q50 was chosen after a side-by-side comparison at q20/q35/q50/q65/q80 — q50 was the lowest setting where text in screenshots stayed legible and gradient backgrounds didn't band.
Use the script for batch conversion:
bash scripts/assets/encode_heic.sh ~/Desktop 1000 50
That walks all PNG/JPEG files in the source directory, resizes to longest-edge 1000px at q50 HEIC, writes outputs to /tmp/heic-out/.
Visual review (mandatory)
Before swapping the new HEICs into resources/, always decode a representative sample back to PNG and visually compare against the source:
sips -s format png /tmp/heic-out/sample.heic --out /tmp/sample-decoded.png >/dev/null 2>&1
open /tmp/sample-decoded.png /Users/you/Desktop/sample.png
Pick the visually most demanding file from the batch — usually one with the most text or the strongest gradients. Confirm:
- No banding in flat color regions
- Text edges still crisp at native display size
- No haloing around anti-aliased edges
- Color rendition matches
If anything looks degraded, bump quality to q60 or q65 and re-batch. The user, not the script, is the final arbiter — show them the sample with sizes before committing.
Bumping quality
If q50 isn't acceptable, the next quality steps are q60 and q65 — beyond that, returns diminish quickly. q80 is the previous default in this repo and roughly 2× the bytes of q50 for no visible improvement on AltTab's content.
Step 5: pbxproj registration
Whenever you change the file extension of a resource (.jpg → .heic, .png → .pdf, etc.), update alt-tab-macos.xcodeproj/project.pbxproj. The places that need patching:
- PBXBuildFile section — comment + the comment inside
fileRef = ... /* name.ext */. - PBXFileReference section — comment,
lastKnownFileType(e.g.image.pdf,image.heic,image.png), andpath = "name.ext". - PBXGroup section — the file's entry inside its parent group's
children. - PBXResourcesBuildPhase section — the entry in the main app target's
files.
For pure extension swaps (no new files), sed -i '' 's|old\.ext|new.ext|g' plus a lastKnownFileType substitution covers it. For new files, generate new 24-char uppercase-hex object IDs (python3 -c "import secrets; print(secrets.token_hex(12).upper())") and insert in all four places.
If you removed an asset entirely (file deleted from disk), delete its 4 entries from pbxproj — otherwise the build fails with "missing file" or ships dangling references.
Step 6: Verify
bash ai/build.sh # must show ** BUILD SUCCEEDED **
bash ai/run.sh # launch the app and visually inspect every asset
Walk through every UI surface that loads an asset:
- Menubar icon (default + the two alternates from Preferences → General → Menubar icon)
- Preferences sidebar — 4 tab icons (SF Symbol on macOS 11+, bundled PDF below)
- Permissions window — open by revoking a permission
- Preferences → Appearance — illustration thumbnails change per show/hide row
Compare git diff --stat before committing. Asset replacements should net negative on bundle size.
Reporting
After the run, report:
- File-by-file before/after sizes for everything that changed.
- Total bundle delta in KB.
- Any files left untouched and why (e.g.,
app.icns— bundle-required format). - The encoder settings used (especially HEIC quality if not q50, so the next person knows).
- Anything the visual review revealed (e.g., "had to bump to q60 for
thumbnails_darkbecause gradient banding at q50").