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;
|
export type ExternalImageService = SharedServiceProps;
|
||||||
|
|
||||||
type LocalImageTransform = {
|
export type LocalImageTransform = {
|
||||||
src: string;
|
src: string;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 });
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -24,6 +24,7 @@ export interface ImageMetadata {
|
||||||
width: number;
|
width: number;
|
||||||
height: number;
|
height: number;
|
||||||
format: ImageInputFormat;
|
format: ImageInputFormat;
|
||||||
|
orientation?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in a new issue