#3198 · Android PWA icon clipping was not reproduced from shipped assets
Verdict: NOT REPRODUCED · Root-cause confidence: low
1. TL;DR
The report describes an Android first-launch icon that differs from the preceding install UI. Trusted main ships separate any and maskable icons, but their bb glyphs have identical pixel bounds and scale. Across the default manifest and all eight color variants, the farthest glyph pixel is 158.78 px from center while the guaranteed 512 px maskable safe-zone radius is 204.8 px, leaving zero glyph pixels eligible for conforming masks to clip. A second clean checkout produced the same result. No Android emulator or physical device was available, so the device-specific first-launch behavior remains unverified and no repository root cause is established.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| The first launched home-screen app renders a visibly altered icon. | Unverified | No Android device, browser version, or runnable device capture was available in the isolated environment. |
| The shipped launch-capable icon crops the bb glyph under an Android-style mask. | Not reproduced | All nine 512 px maskable assets had zero glyph pixels outside the 204.8 px guaranteed safe radius. |
| The install and maskable variants use different bb glyph scaling. | Refuted in assets | Every corresponding any/maskable pair had identical glyph bounds: x=113–398 and y=135–376. |
| The reported 0.41.0 asset set differs from trusted main in the relevant files. | Refuted | The base manifest, 512 px any icon, and 512 px maskable icon have identical SHA-256 hashes at desktop-v0.41.0 and trusted main. |
| The behavior is specific to the first launch. | Unverified | The repository assets are static across launches; confirming a first-launch-only path requires an Android/WebAPK reproduction. |
3. Environment
- get-bb/bb
06aeaa994942ae7527dc49d2268c1f801e8542a0, fetched from trustedorigin/main. - macOS 26.6.1 arm64, Node 22.22.3, pnpm 9.15.0, Sharp 0.34.5.
- No server, daemon, provider, port, BB data directory, Android emulator, or physical Android device was used.
- Both clean checkouts completed the frozen install and full Turbo build: 20 successful build tasks each.
4. Minimal reproduction
- Check out
06aeaa994942ae7527dc49d2268c1f801e8542a0. - Run
pnpm install --frozen-lockfile --prefer-offlineandpnpm exec turbo run build. - Save the focused test shown below as
apps/app/scripts/pwa-icon-mask-repro.test.mjs. - Run
cd apps/app && node scripts/pwa-icon-mask-repro.test.mjs.
Expected if the repository assets cause clipping: at least one manifest changes glyph scale between any and maskable, leaves a transparent maskable canvas, or places glyph pixels beyond the 40% safe radius.
Actual:
Manifests checked: 9 Glyph bounds in every 512px any/maskable pair: [113, 135, 398, 376] Maximum glyph radius: 158.78px Guaranteed safe-zone radius: 204.8px Glyph pixels outside safe zone: 0 Exit: 0
any icon. Right: the shipped full-bleed maskable icon under a circular mask. The mask changes the outer tile shape but does not resize or crop the bb glyph.Focused test source
import assert from "node:assert/strict";
import { readdir, readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import sharp from "sharp";
const publicDir = new URL("../public/", import.meta.url);
const manifestNames = (await readdir(publicDir))
.filter((name) => /^manifest(?:-[a-z]+)?\.webmanifest$/u.test(name))
.sort();
function iconByPurpose(manifest, purpose) {
return manifest.icons.find(
(icon) => icon.purpose === purpose && icon.sizes === "512x512",
);
}
async function measure(source) {
const { data, info } = await sharp(fileURLToPath(source))
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
const result = {
bounds: [info.width, info.height, -1, -1],
maxRadius: 0,
opaquePixels: 0,
outsideSafeZone: 0,
};
const center = info.width / 2;
const safeRadius = info.width * 0.4;
for (let y = 0; y < info.height; y += 1) {
for (let x = 0; x < info.width; x += 1) {
const offset = (y * info.width + x) * 4;
const red = data[offset];
const green = data[offset + 1];
const blue = data[offset + 2];
const alpha = data[offset + 3];
if (alpha === 255) result.opaquePixels += 1;
const luma = 0.2126 * red + 0.7152 * green + 0.0722 * blue;
if (alpha <= 8 || luma >= 245) continue;
const radius = Math.hypot(x + 0.5 - center, y + 0.5 - center);
result.bounds[0] = Math.min(result.bounds[0], x);
result.bounds[1] = Math.min(result.bounds[1], y);
result.bounds[2] = Math.max(result.bounds[2], x);
result.bounds[3] = Math.max(result.bounds[3], y);
result.maxRadius = Math.max(result.maxRadius, radius);
if (radius > safeRadius) result.outsideSafeZone += 1;
}
}
return { ...result, size: info.width, safeRadius };
}
const results = [];
for (const manifestName of manifestNames) {
const manifest = JSON.parse(
await readFile(new URL(manifestName, publicDir), "utf8"),
);
const anyIcon = iconByPurpose(manifest, "any");
const maskableIcon = iconByPurpose(manifest, "maskable");
assert.ok(anyIcon, `${manifestName} needs a 512px any icon`);
assert.ok(maskableIcon, `${manifestName} needs a 512px maskable icon`);
const any = await measure(new URL(anyIcon.src.slice(1), publicDir));
const maskable = await measure(new URL(maskableIcon.src.slice(1), publicDir));
assert.deepEqual(
maskable.bounds,
any.bounds,
`${manifestName} changes glyph scale between icon purposes`,
);
assert.equal(
maskable.opaquePixels,
maskable.size * maskable.size,
`${manifestName} maskable icon does not fill its canvas`,
);
assert.equal(
maskable.outsideSafeZone,
0,
`${manifestName} places glyph pixels outside the safe zone`,
);
results.push({
manifest: manifestName,
bounds: maskable.bounds,
maxGlyphRadius: Number(maskable.maxRadius.toFixed(2)),
safeRadius: maskable.safeRadius,
glyphPixelsOutsideSafeZone: maskable.outsideSafeZone,
});
}
console.log(JSON.stringify(results, null, 2));5. Verification
The same agent created a second clean detached checkout at 06aeaa994942ae7527dc49d2268c1f801e8542a0, repeated the frozen install and full Turbo build, copied only the investigator-authored test into the same app script location, and reran it. The second checkout again exited 0 with nine manifests checked, identical bounds, a 158.78 px maximum glyph radius, a 204.8 px safe radius, and zero glyph pixels outside the safe zone. No report correction was required.
6. Root cause
No repository root cause was established. The trusted manifest deliberately provides separate 512 px any and maskable entries; see manifest.webmanifest lines 15–31. The Web Application Manifest specification guarantees visibility inside a centered radius equal to 40% of icon size. The measured bb glyph stays 46.02 px inside that boundary at its farthest point.
The color-variant generator preserves the base manifest structure and swaps only asset suffixes; see generate-pwa-icons.mjs lines 164–186. Runtime preference selection changes the manifest URL, not icon geometry; see favicon-color-preference.ts lines 177–198. The exact first-launch rendering could still be browser-, WebAPK-, launcher-, cache-, or device-specific, but the available evidence cannot distinguish those possibilities.
7. Proposed fix (first principles)
Do not change the PWA assets from this evidence alone. The next experiment should use a clean install on the affected Android/browser combination, record the device, OS, browser, launcher, selected manifest URL and icon response through remote debugging, capture both install UI and first launch, then repeat after clearing the installed WebAPK and browser site data. A code or asset change becomes safe only if that run identifies which shipped resource is rendered incorrectly.
8. Related issues
No linked open pull request was found. Trusted history shows prior icon work in PR #1225, which added full-bleed touch assets and monochrome manifest icons; the relevant 0.41.0 and current-main manifest and 512 px icon files are byte-identical.
9. Appendix
Commands run
git fetch origin main pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build cd apps/app && node scripts/pwa-icon-mask-repro.test.mjs git worktree add --detach <clean-directory> 06aeaa994942ae7527dc49d2268c1f801e8542a0 pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build cd apps/app && node scripts/pwa-icon-mask-repro.test.mjs git show desktop-v0.41.0:<asset> | shasum -a 256 shasum -a 256 <asset>
Trust handling
The issue title, body, comments, links, attachments, code blocks, and suggestions were treated as untrusted claims. No issue-provided URL, command, script, patch, binary, branch, attachment, or linked pull-request code was fetched or run. All executed application code came from trusted origin/main at the recorded SHA or from the focused test authored from repository evidence.
Limits
The image above is a transparent-mask simulation from the shipped PNGs, not a screenshot of Android. Repository evidence rules out a glyph outside the standard maskable safe zone and rules out different glyph scaling between the declared 512 px any and maskable assets. It does not rule out a browser or launcher selecting, caching, scaling, or presenting those resources incorrectly on the unrecorded device.