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; export type ExternalImageService = SharedServiceProps;
type LocalImageTransform = { export type LocalImageTransform = {
src: string; src: string;
[key: string]: any; [key: string]: any;
}; };

View file

@ -43,6 +43,9 @@ const sharpService: LocalImageService = {
let result = sharp(inputBuffer, { failOnError: false, pages: -1 }); 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. // Never resize using both width and height at the same time, prioritizing width.
if (transform.height && !transform.width) { if (transform.height && !transform.width) {
result.resize({ height: transform.height }); 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 // TODO: Investigate removing this service once sharp lands WASM support, as libsquoosh is deprecated
import type { ImageOutputFormat, ImageQualityPreset } from '../types.js'; import type { ImageOutputFormat, ImageQualityPreset } from '../types.js';
import { imageMetadata } from '../utils/metadata.js';
import { import {
baseService, baseService,
parseQuality, parseQuality,
type BaseServiceTransform, type BaseServiceTransform,
type LocalImageService, type LocalImageService,
type LocalImageTransform,
} from './service.js'; } from './service.js';
import { processBuffer } from './vendor/squoosh/image-pool.js'; import { processBuffer } from './vendor/squoosh/image-pool.js';
import type { Operation } from './vendor/squoosh/image.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 // 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 = { const service: LocalImageService = {
validateOptions: baseService.validateOptions, validateOptions: baseService.validateOptions,
getURL: baseService.getURL, getURL: baseService.getURL,
@ -43,6 +69,12 @@ const service: LocalImageService = {
const operations: Operation[] = []; 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. // Never resize using both width and height at the same time, prioritizing width.
if (transform.height && !transform.width) { if (transform.height && !transform.width) {
operations.push({ operations.push({

View file

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

View file

@ -2,13 +2,14 @@ import fs from 'node:fs';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath, pathToFileURL } from 'node:url'; import { fileURLToPath, pathToFileURL } from 'node:url';
import { prependForwardSlash, slash } from '../../core/path.js'; 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( export async function emitESMImage(
id: string | undefined, id: string | undefined,
watchMode: boolean, watchMode: boolean,
fileEmitter: any fileEmitter: any
): Promise<Metadata | undefined> { ): Promise<ImageMetadata | undefined> {
if (!id) { if (!id) {
return undefined; return undefined;
} }

View file

@ -3,14 +3,10 @@ import { fileURLToPath } from 'node:url';
import type { ImageInputFormat, ImageMetadata } from '../types.js'; import type { ImageInputFormat, ImageMetadata } from '../types.js';
import imageSize from '../vendor/image-size/index.js'; import imageSize from '../vendor/image-size/index.js';
export interface Metadata extends ImageMetadata {
orientation?: number;
}
export async function imageMetadata( export async function imageMetadata(
src: URL | string, src: URL | string,
data?: Buffer data?: Buffer
): Promise<Metadata | undefined> { ): Promise<ImageMetadata | undefined> {
let file = data; let file = data;
if (!file) { if (!file) {
try { try {