Skip to content

fix(miniflare): apply EXIF orientation in local Images binding and cf.image transforms - #15031

Open
vdanielb wants to merge 1 commit into
cloudflare:mainfrom
vdanielb:fix/images-local-exif-orientation
Open

fix(miniflare): apply EXIF orientation in local Images binding and cf.image transforms#15031
vdanielb wants to merge 1 commit into
cloudflare:mainfrom
vdanielb:fix/images-local-exif-orientation

Conversation

@vdanielb

@vdanielb vdanielb commented Aug 5, 2026

Copy link
Copy Markdown

Fixes #15029

What this PR solves / how to test

The local (low-fidelity) Images binding polyfill decoded images as stored, without applying their EXIF orientation flag. Production auto-orients before transforming, so phone photos (stored as landscape pixels + EXIF Orientation: 6) rendered sideways in wrangler dev but upright when deployed. Downstream, this made Astro's @astrojs/cloudflare adapter show EXIF-rotated photos sideways in astro dev.

This PR decodes with sharp's autoOrient input option in both local fetchers:

  • imagesLocalFetcher (env.IMAGES.input(...).transform(...))
  • cfImageLocalFetcher (fetch(url, { cf: { image } })), including the format: "json" path

autoOrient bakes the EXIF rotation into the pixels at decode and clears the flag; an explicit user rotate still composes on top. The /info path is intentionally unchanged (it keeps reporting the stored dimensions from metadata).

To test: run any Worker with an images binding and transform a JPEG that carries EXIF Orientation: 6 (e.g. an iPhone portrait photo). Before this change the local output is sideways; after, it is upright and matches wrangler dev --remote / production.

Includes regression tests in transform.spec.ts that tag the existing fixture with EXIF orientation 6 and assert the output is upright (dimensions swapped, rotated pixel layout) and that resize applies to the upright image.

Author has addressed the following

  • Tests
    • TESTED: added regression tests in packages/miniflare/test/plugins/images/transform.spec.ts; ran test/plugins/images/ and test/plugins/core/cf-image.spec.ts locally (34 passed)
  • Public documentation
    • Not necessary because: this fixes a local/production fidelity bug; no documented behavior changes

Open in Devin Review

….image transforms

Production auto-orients images per their EXIF orientation flag before
applying transforms, but the local sharp-based polyfill decoded the raw
pixels as stored, so phone photos (stored sideways + rotation flag) came
back rotated 90deg in local dev while rendering correctly when deployed.

Decode with sharp's autoOrient option in both the Images binding and
cf.image local fetchers, baking the EXIF rotation into the pixels before
user transforms, matching production. The /info path is unchanged.

Fixes cloudflare#15029

Co-authored-by: Cursor <cursoragent@cursor.com>
@changeset-bot

changeset-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 1219831

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 8 packages
Name Type
miniflare Patch
@cloudflare/deploy-helpers Patch
@cloudflare/pages-shared Patch
@cloudflare/remote-bindings Patch
@cloudflare/runtime-types Patch
@cloudflare/vite-plugin Patch
@cloudflare/vitest-pool-workers Patch
wrangler Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@workers-devprod
workers-devprod requested review from a team and jamesopstad and removed request for a team August 5, 2026 06:11
@workers-devprod

Copy link
Copy Markdown
Contributor

Codeowners approval required for this PR:

  • @cloudflare/wrangler
Show detailed file reviewers
  • .changeset/images-local-exif-orientation.md: [@cloudflare/wrangler]
  • packages/miniflare/src/plugins/images/fetcher.ts: [@cloudflare/wrangler]
  • packages/miniflare/test/plugins/images/transform.spec.ts: [@cloudflare/wrangler]

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

Comment on lines +213 to +234
test("EXIF orientation is applied before transforms (matches production)", async ({
expect,
}) => {
// Production auto-orients per EXIF before transforming; without it the
// 200x100 source would pass through sideways as 200x100.
const { body } = await transform({}, "image/png", await exifRotatedJpeg());
const meta = await sharp(body).metadata();
expect(meta.width).toBe(100);
expect(meta.height).toBe(200);

// After the 90° CW rotation the source's red bottom half lands on the
// left and the white top half on the right. JPEG encoding is lossy, so
// sample deep inside each half and allow small artifacts.
const left = await pixelAt(body, 25, 100);
expect(left.r).toBeGreaterThan(240);
expect(left.g).toBeLessThan(15);
expect(left.b).toBeLessThan(15);
const right = await pixelAt(body, 75, 100);
expect(right.r).toBeGreaterThan(240);
expect(right.g).toBeGreaterThan(240);
expect(right.b).toBeGreaterThan(240);
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: No regression test added for the cf.image path

The PR changes both imagesLocalFetcher and cfImageLocalFetcher, but the added regression tests only exercise the env.IMAGES binding path in packages/miniflare/test/plugins/images/transform.spec.ts. The cf.image auto-orient behaviour (including the format: "json" branch) is untested; test/plugins/core/cf-image.spec.ts would be the natural home for an equivalent EXIF orientation case.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vdanielb could you add tests for cf.image too? 🙂

Comment on lines +93 to +98
async function exifRotatedJpeg() {
return sharp(sourcePng)
.jpeg({ quality: 95 })
.withMetadata({ orientation: 6 })
.toBuffer();
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📝 Info: Test fixture relies on withMetadata's orientation option

withMetadata({ orientation: 6 }) is the older sharp API (superseded by keepMetadata()/withExif()/withMetadata variants in recent versions). It still works in sharp 0.35.x but is a deprecation candidate; if it ever becomes a no-op the tests would silently pass against the un-fixed code path since a JPEG without the orientation tag decodes identically with and without autoOrient. Asserting the fixture actually carries orientation: 6 (via sharp(buf).metadata()) would make the regression tests self-verifying.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vdanielb could you add this assert? 🙏


if (options.format === "json") {
const jsonTransformer = sharp(source);
const jsonTransformer = sharp(source, { autoOrient: true });

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 cf.image format=json reports stored (non-oriented) original dimensions alongside oriented output dimensions

The format: "json" response mixes two coordinate systems: width/height come from the auto-oriented transformer, but original.width/original.height come from sharp(source).metadata() at packages/miniflare/src/plugins/images/fetcher.ts:397, which is not auto-oriented. For an EXIF orientation-6 photo this yields e.g. {width: 100, height: 200, original: {width: 200, height: 100}}. Production's cf.image JSON almost certainly reports the display-oriented original dimensions; if so this is a remaining fidelity gap in the same area this PR is fixing.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vdanielb could you update this too? 😄


if (url.pathname == "/info") {
return runInfo(transformer);
return runInfo(sharp(source, {}));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Transform output dimensions now disagree with info() for EXIF-rotated images

/info intentionally keeps the non-auto-oriented sharp(source, {}) (packages/miniflare/src/plugins/images/fetcher.ts:69) while the transform path now decodes with autoOrient: true. For a JPEG with EXIF orientation 6, env.IMAGES.info() will report 200x100 while .transform({}).output(...) now yields 100x200. Worth confirming against production: if the production info endpoint reports the display (EXIF-applied) dimensions, local dev will now be inconsistent with production in the opposite direction from the bug this PR fixes. No test covers /info with an EXIF-tagged image.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've manually tested this and I can confirm that in production env.IMAGES.info() differ between production and the local simulation

in production the original dimensions are shown while locally those after the EXIF

@vdanielb could you address fix and also add test to cover /info? 🙏

@pkg-pr-new

pkg-pr-new Bot commented Aug 6, 2026

Copy link
Copy Markdown
@cloudflare/autoconfig

npm i https://pkg.pr.new/@cloudflare/autoconfig@15031

@cloudflare/build-output-utils

npm i https://pkg.pr.new/@cloudflare/build-output-utils@15031

@cloudflare/config

npm i https://pkg.pr.new/@cloudflare/config@15031

create-cloudflare

npm i https://pkg.pr.new/create-cloudflare@15031

@cloudflare/deploy-helpers

npm i https://pkg.pr.new/@cloudflare/deploy-helpers@15031

@cloudflare/kv-asset-handler

npm i https://pkg.pr.new/@cloudflare/kv-asset-handler@15031

miniflare

npm i https://pkg.pr.new/miniflare@15031

@cloudflare/pages-functions

npm i https://pkg.pr.new/@cloudflare/pages-functions@15031

@cloudflare/pages-shared

npm i https://pkg.pr.new/@cloudflare/pages-shared@15031

@cloudflare/unenv-preset

npm i https://pkg.pr.new/@cloudflare/unenv-preset@15031

@cloudflare/vite-plugin

npm i https://pkg.pr.new/@cloudflare/vite-plugin@15031

@cloudflare/vitest-pool-workers

npm i https://pkg.pr.new/@cloudflare/vitest-pool-workers@15031

@cloudflare/workers-auth

npm i https://pkg.pr.new/@cloudflare/workers-auth@15031

@cloudflare/workers-editor-shared

npm i https://pkg.pr.new/@cloudflare/workers-editor-shared@15031

@cloudflare/workers-utils

npm i https://pkg.pr.new/@cloudflare/workers-utils@15031

wrangler

npm i https://pkg.pr.new/wrangler@15031

commit: 1219831

@dario-piotrowicz
dario-piotrowicz requested review from dario-piotrowicz and removed request for jamesopstad August 6, 2026 13:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Untriaged

Development

Successfully merging this pull request may close these issues.

🐛 BUG: Local Images binding ignores EXIF orientation - photos render sideways in local dev

3 participants