# feat\(server\): report image dimensions with signed asset URLs · PR \#10198 · pingdotgg/t3code

[View on GitCafe](https://git.cafe/pingdotgg/t3code/pull/10198)

Repository: [pingdotgg/t3code](https://git.cafe/pingdotgg/t3code)

Visibility: public

State: merged

Source repository: pingdotgg/t3code

Source branch: media-dims/contract

Target branch: main

Head commit: 0c0d4ea243cbc281d399a60588a9419eb9a5a646

[Discussion and reviews](https://git.cafe/pingdotgg/t3code/pull/10198)

[Commits](https://git.cafe/pingdotgg/t3code/pull/10198/commits?format=markdown)

[Files changed](https://git.cafe/pingdotgg/t3code/pull/10198/diffs?format=markdown)

## Description

---

Stack 1/3 (#10201). Follow-ups: #10199 (mobile), #10200 (web).

## Problem

Both clients reserve a 16:9 slot for a chat image while its bytes load, then resize to the natural size once decoded. That last resize moves every row below the image, and on mobile it can push the end of the thread behind the composer or leave the next tool call drawn under the image (see #10199). The clients can't do better without knowing the size up front.

## Fix

The server already opens the file when it issues a signed asset URL, so it now reads the pixel size from the PNG, JPEG, GIF, or WebP header and returns it as an optional `imageDimensions` on `AssetCreateUrlResult`. `packages/client-runtime` threads it onto `AssetUrlState.Success` so both clients see it.

- `packages/shared/src/imageDimensions.ts` — dependency-free header parser. JPEG honours the EXIF orientation tag (5–8 swap the axes), since every viewer applies it before display and phone photos are stored on their side.
- For `media-file` resources the header is read through the same identity-checked handle used for the inode check, so there's no second open to swap. Only formats the parser understands are read (SVG, AVIF, ICO skip).
- Best effort: unreadable or unsupported files omit the field and the client falls back to measuring after decode. Older clients ignore the field; older servers omit it.

## Compatibility

Additive optional field. `Schema.optional` on the contract; unknown keys are ignored by older clients.

## Verification

- `packages/shared`: `imageDimensions.test.ts` — PNG, GIF, JPEG (with APP1 prefix, Huffman-table marker skip, EXIF orientation 6 vs 1), all three WebP flavours, truncated/zero-size/unsupported input. 7 passed.
- `apps/server`: `AssetAccess.test.ts` — a real PNG header reports 1600×900, an mp4 and a malformed `.png` omit the field. 27 passed.
- Typecheck clean for `packages/shared`, `packages/contracts`, `packages/client-runtime`, `apps/server`.

No visual change on its own; screenshots are on the client PRs.

Model: Claude Fable 5 · Harness: Claude Code in T3 Code

🤖 Generated with [Claude Code](https://claude.com/claude-code)

<!-- Macroscope's pull request summary starts here -->
<!-- Macroscope will only edit the content between these invisible markers, and the markers themselves will not be visible in the GitHub rendered markdown. -->
<!-- If you delete either of the start / end markers from your PR's description, Macroscope will append its summary at the bottom of the description. -->
> [!NOTE]
> ### Report image dimensions with signed asset URLs
> - Adds a shared image-header parser in [imageDimensions.ts](https://github.com/pingdotgg/t3code/pull/10198/files#diff-6cec4dc7d220f87973a07e4e1acdc952d88bfe4654b405404e192227ede6c7cc) that extracts pixel width and height from PNG, GIF, WebP, and JPEG headers, returning `null` for unsupported or malformed input. JPEG parsing also applies EXIF orientation axis swaps for values 5-8.
> - Integrates parsing into [AssetAccess.ts](https://github.com/pingdotgg/t3code/pull/10198/files#diff-7106051c89d4fb8e7cebeee854e81bb55fdf03ec154089eb8c7499ef78ae51c1) so `issueAssetUrl` reads a bounded header prefix from already-open media handles for supported extensions (png, jpg, jpeg, gif, webp) and includes `imageDimensions` in the result when parsing succeeds.
> - Updates [assets.ts](https://github.com/pingdotgg/t3code/pull/10198/files#diff-be151844cc8699bd23b42fc4d6d73c29e32170c1b03cb89b86296402f2c40359) contracts and [assets.ts](https://github.com/pingdotgg/t3code/pull/10198/files#diff-e4d6b86d6446338ff104d18e3def1fd2c404c0cb4c3596f5b1cccf6a58fc8227) client state to carry the optional `imageDimensions` field through successful asset URL results.
> - Risk: dimension extraction is best-effort and never blocks URL issuance, but callers reading `AssetCreateUrlResult` or `AssetUrlState.Success` should treat the new optional `imageDimensions` field as potentially absent.
>
> <!-- Macroscope's review summary starts here -->
>
> <sup><a href="https://app.macroscope.com">Macroscope</a> summarized 0c0d4ea.</sup>
> <!-- Macroscope's review summary ends here -->
>
<!-- Macroscope's pull request summary ends here -->
