fix(assets): Fix images not following EXIF rotation (#7637)

This commit is contained in:
Erika 2023-07-13 21:38:57 +02:00 committed by GitHub
parent c90de81373
commit af5827d4f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 46 additions and 8 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fix `astro:assets` not respecting EXIF rotation

View file

@ -56,7 +56,7 @@ interface SharedServiceProps {
export type ExternalImageService = SharedServiceProps;
type LocalImageTransform = {
export type LocalImageTransform = {
src: string;
[key: string]: any;
};

View file

@ -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 });

View file

@ -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({

View file

@ -24,6 +24,7 @@ export interface ImageMetadata {
width: number;
height: number;
format: ImageInputFormat;
orientation?: number;
}
/**

View file

@ -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;
}

View file

@ -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 {