From af5827d4f7af9437c0c3fcff5c0239577aa68498 Mon Sep 17 00:00:00 2001 From: Erika <3019731+Princesseuh@users.noreply.github.com> Date: Thu, 13 Jul 2023 21:38:57 +0200 Subject: [PATCH] fix(assets): Fix images not following EXIF rotation (#7637) --- .changeset/fluffy-timers-remain.md | 5 +++ packages/astro/src/assets/services/service.ts | 2 +- packages/astro/src/assets/services/sharp.ts | 3 ++ packages/astro/src/assets/services/squoosh.ts | 32 +++++++++++++++++++ packages/astro/src/assets/types.ts | 1 + packages/astro/src/assets/utils/emitAsset.ts | 5 +-- packages/astro/src/assets/utils/metadata.ts | 6 +--- 7 files changed, 46 insertions(+), 8 deletions(-) create mode 100644 .changeset/fluffy-timers-remain.md diff --git a/.changeset/fluffy-timers-remain.md b/.changeset/fluffy-timers-remain.md new file mode 100644 index 000000000..80d52cb2e --- /dev/null +++ b/.changeset/fluffy-timers-remain.md @@ -0,0 +1,5 @@ +--- +'astro': patch +--- + +Fix `astro:assets` not respecting EXIF rotation diff --git a/packages/astro/src/assets/services/service.ts b/packages/astro/src/assets/services/service.ts index c5f9cf935..8b7611db2 100644 --- a/packages/astro/src/assets/services/service.ts +++ b/packages/astro/src/assets/services/service.ts @@ -56,7 +56,7 @@ interface SharedServiceProps { export type ExternalImageService = SharedServiceProps; -type LocalImageTransform = { +export type LocalImageTransform = { src: string; [key: string]: any; }; diff --git a/packages/astro/src/assets/services/sharp.ts b/packages/astro/src/assets/services/sharp.ts index f77a87654..7f70b9268 100644 --- a/packages/astro/src/assets/services/sharp.ts +++ b/packages/astro/src/assets/services/sharp.ts @@ -43,6 +43,9 @@ const sharpService: LocalImageService = { let result = sharp(inputBuffer, { failOnError: false, pages: -1 }); + // always call rotate to adjust for EXIF data orientation + result.rotate(); + // Never resize using both width and height at the same time, prioritizing width. if (transform.height && !transform.width) { result.resize({ height: transform.height }); diff --git a/packages/astro/src/assets/services/squoosh.ts b/packages/astro/src/assets/services/squoosh.ts index d9307a219..2ed127ade 100644 --- a/packages/astro/src/assets/services/squoosh.ts +++ b/packages/astro/src/assets/services/squoosh.ts @@ -1,11 +1,13 @@ // TODO: Investigate removing this service once sharp lands WASM support, as libsquoosh is deprecated import type { ImageOutputFormat, ImageQualityPreset } from '../types.js'; +import { imageMetadata } from '../utils/metadata.js'; import { baseService, parseQuality, type BaseServiceTransform, type LocalImageService, + type LocalImageTransform, } from './service.js'; import { processBuffer } from './vendor/squoosh/image-pool.js'; import type { Operation } from './vendor/squoosh/image.js'; @@ -28,6 +30,30 @@ const qualityTable: Record< // Squoosh's PNG encoder does not support a quality setting, so we can skip that here }; +async function getRotationForEXIF( + transform: LocalImageTransform, + inputBuffer: Buffer +): Promise { + // check EXIF orientation data and rotate the image if needed + const meta = await imageMetadata(transform.src, inputBuffer); + + if (!meta) return undefined; + + // EXIF orientations are a bit hard to read, but the numbers are actually standard. See https://exiftool.org/TagNames/EXIF.html for a list. + // Various illustrations can also be found online for a more graphic representation, it's a bit old school. + switch (meta.orientation) { + case 3: + case 4: + return { type: 'rotate', numRotations: 2 }; + case 5: + case 6: + return { type: 'rotate', numRotations: 1 }; + case 7: + case 8: + return { type: 'rotate', numRotations: 3 }; + } +} + const service: LocalImageService = { validateOptions: baseService.validateOptions, getURL: baseService.getURL, @@ -43,6 +69,12 @@ const service: LocalImageService = { const operations: Operation[] = []; + const rotation = await getRotationForEXIF(transform, inputBuffer); + + if (rotation) { + operations.push(rotation); + } + // Never resize using both width and height at the same time, prioritizing width. if (transform.height && !transform.width) { operations.push({ diff --git a/packages/astro/src/assets/types.ts b/packages/astro/src/assets/types.ts index 31a7106ec..d4a8c4c89 100644 --- a/packages/astro/src/assets/types.ts +++ b/packages/astro/src/assets/types.ts @@ -24,6 +24,7 @@ export interface ImageMetadata { width: number; height: number; format: ImageInputFormat; + orientation?: number; } /** diff --git a/packages/astro/src/assets/utils/emitAsset.ts b/packages/astro/src/assets/utils/emitAsset.ts index b1e01b3e4..e1d66dfc7 100644 --- a/packages/astro/src/assets/utils/emitAsset.ts +++ b/packages/astro/src/assets/utils/emitAsset.ts @@ -2,13 +2,14 @@ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { prependForwardSlash, slash } from '../../core/path.js'; -import { imageMetadata, type Metadata } from './metadata.js'; +import type { ImageMetadata } from '../types.js'; +import { imageMetadata } from './metadata.js'; export async function emitESMImage( id: string | undefined, watchMode: boolean, fileEmitter: any -): Promise { +): Promise { if (!id) { return undefined; } diff --git a/packages/astro/src/assets/utils/metadata.ts b/packages/astro/src/assets/utils/metadata.ts index 8d2b5c6a2..8d1f1bc4a 100644 --- a/packages/astro/src/assets/utils/metadata.ts +++ b/packages/astro/src/assets/utils/metadata.ts @@ -3,14 +3,10 @@ import { fileURLToPath } from 'node:url'; import type { ImageInputFormat, ImageMetadata } from '../types.js'; import imageSize from '../vendor/image-size/index.js'; -export interface Metadata extends ImageMetadata { - orientation?: number; -} - export async function imageMetadata( src: URL | string, data?: Buffer -): Promise { +): Promise { let file = data; if (!file) { try {