Skip to content

Commit a079697

Browse files
committed
Resize avatar image down rather than up
1 parent a184c89 commit a079697

4 files changed

Lines changed: 72 additions & 43 deletions

File tree

astro.config.ts

Lines changed: 63 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,18 @@ const optimizeImagesIntegration: AstroIntegration = {
3030
name: "optimize-images",
3131
hooks: {
3232
async "astro:build:done"({ dir, routes }) {
33-
type Images = { original: string; webp: string | string[] }[];
33+
type Resize = "" | "up" | "down";
34+
type Images = { original: string; webp: string[]; resize: Resize }[];
3435
type MatchGroups = {
35-
resize?: string;
36+
resize: Resize;
3637
preSrc: string;
3738
src: string;
3839
postSrc: string;
3940
};
4041

41-
const resizeSizes = [1, 1.5, 2, 3, 4];
42+
const resizeSuffixes = ["a", "b", "c", "d", "e"];
43+
const resizeUpMultipliers = [1, 1.5, 2, 3, 4];
44+
const resizeDownMultipliers = [0.25, 0.375, 0.5, 0.75, 1];
4245
const images: Images = [];
4346

4447
for (const route of routes) {
@@ -49,29 +52,33 @@ const optimizeImagesIntegration: AstroIntegration = {
4952
const htmlPath = nodeUrl.fileURLToPath(route.distURL);
5053
let html = await fs.readFile(htmlPath, "utf-8");
5154
const matches = html.matchAll(
52-
/<img optimize-image (?<resize>resize="true")?(?<preSrc>.+)src="(?<src>[^"]+)"(?<postSrc>.+)>/g
55+
/<img optimize-image resize="(?<resize>[a-z-]*)"(?<preSrc>.+)src="(?<src>[^"]+)"(?<postSrc>.+)>/g
5356
);
5457

5558
for (const match of matches) {
5659
const { resize, preSrc, src, postSrc } = match.groups as MatchGroups;
57-
const baseName = src.slice(0, src.lastIndexOf("."));
60+
const extensionlessPath = src.slice(0, src.lastIndexOf("."));
5861
let webp;
5962

60-
if (resize) {
61-
webp = resizeSizes.map((size) => `${baseName}-${size}.webp`);
63+
if (resize === "") {
64+
webp = [`${extensionlessPath}.webp`];
6265
} else {
63-
webp = `${baseName}.webp`;
66+
webp = resizeSuffixes.map(
67+
(suffix) => `${extensionlessPath}${suffix}.webp`
68+
);
6469
}
6570

66-
images.push({ original: src, webp });
71+
images.push({ original: src, webp, resize });
6772

6873
html = html.replace(
6974
match[0],
7075
`
7176
<source srcset="${
72-
typeof webp === "string"
73-
? webp
74-
: webp.map((fileName, i) => `${fileName} ${resizeSizes[i]}x`)
77+
webp.length === 1
78+
? webp[0]
79+
: webp.map(
80+
(filePath, i) => `${filePath} ${resizeUpMultipliers[i]}x`
81+
)
7582
}" type="image/webp">
7683
<img ${preSrc} src="${src}" ${postSrc}>
7784
`
@@ -86,27 +93,34 @@ const optimizeImagesIntegration: AstroIntegration = {
8693
name: "astro-optimize-images",
8794
create: true
8895
}) as string;
89-
for (let { original, webp } of images) {
96+
const sharpOptions = { limitInputPixels: false };
97+
const imageOptions = {
98+
png: { compressionLevel: 9, quality: 80 },
99+
webp: { effort: 6 }
100+
};
101+
for (let { original, webp, resize } of images) {
90102
original = path.join(distDir, original);
103+
webp = webp.map((filePath) => path.join(distDir, filePath));
91104

92-
if (typeof webp === "string") {
93-
webp = [path.join(distDir, webp)];
94-
} else {
95-
webp = webp.map((fileName) => path.join(distDir, fileName));
96-
}
97-
98-
const format = path.extname(original).slice(1);
105+
const format = path.extname(original).slice(1) as "gif" | "png";
99106
const image = sharp(original, {
100-
limitInputPixels: false,
107+
...sharpOptions,
101108
animated: format === "gif"
102109
});
103-
const { width, height } = await image.metadata();
104110

111+
const { width, height } = await image.metadata();
112+
const multiply =
113+
webp.length > 1 &&
114+
typeof width === "number" &&
115+
typeof height === "number";
116+
const multipliers =
117+
resize === "up" ? resizeUpMultipliers : resizeDownMultipliers;
105118
for (let i = 0; i < webp.length; i++) {
106119
const cachedWebp = path.join(
107120
cacheDir,
108121
path.basename(webp[i] as string)
109122
);
123+
110124
try {
111125
await fs.access(cachedWebp);
112126

@@ -116,24 +130,36 @@ const optimizeImagesIntegration: AstroIntegration = {
116130
} catch {
117131
const clone = image.clone();
118132

119-
if (typeof width === "number" && typeof height === "number") {
133+
if (multiply) {
120134
clone.resize(
121-
width * (resizeSizes[i] as number),
122-
height * (resizeSizes[i] as number)
135+
width * (multipliers[i] as number),
136+
height * (multipliers[i] as number)
123137
);
124138
}
125139

126-
await clone
127-
.webp({
128-
effort: 6,
129-
// For some reason, using lossless compression mode on large
130-
// GIFs increase their WebP size.
131-
lossless:
132-
format === "gif" &&
133-
// Less than 1MB
134-
(await fs.stat(original)).size / 1000 < 1000
135-
})
136-
.toFile(webp[i] as string);
140+
// Compressing images in their original format before converting
141+
// them to WebP reduces the WebP file size. GIFs are compressed
142+
// using Gifsicle instead of Sharp, so that step is skipped for GIFs
143+
// for now.
144+
if (format !== "gif") {
145+
await sharp(
146+
await clone.toFormat(format, imageOptions[format]).toBuffer(),
147+
sharpOptions
148+
)
149+
.webp(imageOptions.webp)
150+
.toFile(webp[i] as string);
151+
} else {
152+
await clone
153+
.webp({
154+
...imageOptions.webp,
155+
// For some reason, using lossless compression mode on large
156+
// GIFs increase their WebP size but reduces the size of
157+
// smaller GIFs, so it is turned on for GIFs smaller than 1MB.
158+
lossless: (await fs.stat(original)).size / 1000 < 1000
159+
})
160+
.toFile(webp[i] as string);
161+
}
162+
137163
await fs.copyFile(webp[i] as string, cachedWebp);
138164
}
139165
}
@@ -161,9 +187,7 @@ const optimizeImagesIntegration: AstroIntegration = {
161187
case "png": {
162188
await fs.writeFile(
163189
original,
164-
await image
165-
.png({ compressionLevel: 9, palette: true })
166-
.toBuffer(),
190+
await image.png(imageOptions.png).toBuffer(),
167191
"binary"
168192
);
169193

src/assets/avatar.png

128 KB
Loading

src/components/about/hero.astro

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import avatarPng from "@assets/avatar.png";
55
---
66

77
<section>
8-
<Picture resize={true} alt="mcecode's avatar" {...avatarPng} />
8+
<Picture resize="down" alt="mcecode's avatar" {...avatarPng} />
99
<h1>Hi, I'm Matthew</h1>
1010
<p>I code to turn ideas into reality</p>
1111
</section>

src/components/ui/picture.astro

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ type Props = {
55
height: number;
66
alt: string;
77
class?: string | { picture: string; img: string };
8-
resize?: boolean;
8+
resize?: "up" | "down";
99
format?: string;
1010
};
1111
@@ -21,7 +21,12 @@ if (typeof Astro.props.class === "object") {
2121
imgClass = Astro.props.class.img;
2222
}
2323
24-
const { resize = false } = Astro.props;
24+
const { resize = "" } = Astro.props;
25+
26+
if (resize === "down") {
27+
Astro.props.width = Astro.props.width * 0.25;
28+
Astro.props.height = Astro.props.height * 0.25;
29+
}
2530
2631
delete Astro.props.class;
2732
delete Astro.props.resize;
@@ -36,5 +41,5 @@ delete Astro.props.format;
3641
there should be none.
3742
*/}
3843
<!-- @ts-expect-error -->
39-
<img optimize-image resize={resize} class={imgClass} decoding="async" loading="lazy" {...Astro.props} />
44+
<img optimize-image resize=`${resize}` class={imgClass} decoding="async" loading="lazy" {...Astro.props} />
4045
</picture>

0 commit comments

Comments
 (0)