1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
|
import type { FitEnum, FormatEnum, SharpOptions } from 'sharp';
import { AstroError, AstroErrorData } from '../../core/errors/index.js';
import type { ImageFit, ImageOutputFormat, ImageQualityPreset } from '../types.js';
import {
type BaseServiceTransform,
type LocalImageService,
baseService,
parseQuality,
} from './service.js';
export interface SharpImageServiceConfig {
/**
* The `limitInputPixels` option passed to Sharp. See https://sharp.pixelplumbing.com/api-constructor for more information
*/
limitInputPixels?: SharpOptions['limitInputPixels'];
}
let sharp: typeof import('sharp');
const qualityTable: Record<ImageQualityPreset, number> = {
low: 25,
mid: 50,
high: 80,
max: 100,
};
async function loadSharp() {
let sharpImport: typeof import('sharp');
try {
sharpImport = (await import('sharp')).default;
} catch {
throw new AstroError(AstroErrorData.MissingSharp);
}
// Disable the `sharp` `libvips` cache as it errors when the file is too small and operations are happening too fast (runs into a race condition) https://github.com/lovell/sharp/issues/3935#issuecomment-1881866341
sharpImport.cache(false);
return sharpImport;
}
const fitMap: Record<ImageFit, keyof FitEnum> = {
fill: 'fill',
contain: 'inside',
cover: 'cover',
none: 'outside',
'scale-down': 'inside',
outside: 'outside',
inside: 'inside',
};
const sharpService: LocalImageService<SharpImageServiceConfig> = {
validateOptions: baseService.validateOptions,
getURL: baseService.getURL,
parseURL: baseService.parseURL,
getHTMLAttributes: baseService.getHTMLAttributes,
getSrcSet: baseService.getSrcSet,
async transform(inputBuffer, transformOptions, config) {
if (!sharp) sharp = await loadSharp();
const transform: BaseServiceTransform = transformOptions as BaseServiceTransform;
// Return SVGs as-is
// TODO: Sharp has some support for SVGs, we could probably support this once Sharp is the default and only service.
if (transform.format === 'svg') return { data: inputBuffer, format: 'svg' };
const result = sharp(inputBuffer, {
failOnError: false,
pages: -1,
limitInputPixels: config.service.config.limitInputPixels,
});
// always call rotate to adjust for EXIF data orientation
result.rotate();
// If `fit` isn't set then use old behavior:
// - Do not use both width and height for resizing, and prioritize width over height
// - Allow enlarging images
const withoutEnlargement = Boolean(transform.fit);
if (transform.width && transform.height && transform.fit) {
const fit: keyof FitEnum = fitMap[transform.fit] ?? 'inside';
result.resize({
width: Math.round(transform.width),
height: Math.round(transform.height),
fit,
position: transform.position,
withoutEnlargement,
});
} else if (transform.height && !transform.width) {
result.resize({
height: Math.round(transform.height),
withoutEnlargement,
});
} else if (transform.width) {
result.resize({
width: Math.round(transform.width),
withoutEnlargement,
});
}
if (transform.format) {
let quality: number | string | undefined = undefined;
if (transform.quality) {
const parsedQuality = parseQuality(transform.quality);
if (typeof parsedQuality === 'number') {
quality = parsedQuality;
} else {
quality = transform.quality in qualityTable ? qualityTable[transform.quality] : undefined;
}
}
result.toFormat(transform.format as keyof FormatEnum, { quality: quality });
}
const { data, info } = await result.toBuffer({ resolveWithObject: true });
return {
data: data,
format: info.format as ImageOutputFormat,
};
},
};
export default sharpService;
|