fix(assets): Fix images not following EXIF rotation (#7637)
This commit is contained in:
parent
c90de81373
commit
af5827d4f7
7 changed files with 46 additions and 8 deletions
5
.changeset/fluffy-timers-remain.md
Normal file
5
.changeset/fluffy-timers-remain.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Fix `astro:assets` not respecting EXIF rotation
|
|
@ -56,7 +56,7 @@ interface SharedServiceProps {
|
|||
|
||||
export type ExternalImageService = SharedServiceProps;
|
||||
|
||||
type LocalImageTransform = {
|
||||
export type LocalImageTransform = {
|
||||
src: string;
|
||||
[key: string]: any;
|
||||
};
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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<Operation | undefined> {
|
||||
// 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({
|
||||
|
|
|
@ -24,6 +24,7 @@ export interface ImageMetadata {
|
|||
width: number;
|
||||
height: number;
|
||||
format: ImageInputFormat;
|
||||
orientation?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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<Metadata | undefined> {
|
||||
): Promise<ImageMetadata | undefined> {
|
||||
if (!id) {
|
||||
return undefined;
|
||||
}
|
||||
|
|
|
@ -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<Metadata | undefined> {
|
||||
): Promise<ImageMetadata | undefined> {
|
||||
let file = data;
|
||||
if (!file) {
|
||||
try {
|
||||
|
|
Loading…
Reference in a new issue