@xingwangzhe/stalux 1.51.0 → 1.51.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xingwangzhe/stalux",
3
- "version": "1.51.0",
3
+ "version": "1.51.1",
4
4
  "description": "A powerful, modern Astro blog theme — use as template or install as plugin",
5
5
  "keywords": [
6
6
  "astro",
@@ -111,12 +111,12 @@
111
111
  "@mcp-b/webmcp-polyfill": "^5.1.0",
112
112
  "@microsoft/clarity": "1.0.2",
113
113
  "@pagefind/component-ui": "^1.5.2",
114
- "@resvg/resvg-js": "2.6.2",
115
114
  "@swup/a11y-plugin": "^5.2.1",
116
115
  "@swup/head-plugin": "^2.3.1",
117
116
  "@swup/js-plugin": "^3.2.0",
118
117
  "@swup/preload-plugin": "^3.2.12",
119
118
  "@swup/scroll-plugin": "^4.0.0",
119
+ "@takumi-rs/core": "2.17.1",
120
120
  "@waline/client": "^3.16.1",
121
121
  "@xingwangzhe/cjk-font-split-native": "^0.4.10",
122
122
  "@xingwangzhe/satteri-mermaid": "^0.8.2",
@@ -131,7 +131,6 @@
131
131
  "pagefind": "^1.5.2",
132
132
  "parse5": "^8.0.1",
133
133
  "photoswipe": "^5.4.4",
134
- "satori": "0.63.2",
135
134
  "satteri": "^0.10.5",
136
135
  "simple-icons": "^16.35.0",
137
136
  "swup": "4.10.0",
@@ -2,8 +2,6 @@ import { createHash } from "node:crypto";
2
2
  import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import { imageSize } from "image-size";
5
- import { loadSharp, type SharpRawBuffer } from "./sharp-loader";
6
- import { CARD_HEIGHT, COVER_WIDTH } from "./social-card-layout";
7
5
 
8
6
  export interface SocialCardPaths {
9
7
  publicDir: string;
@@ -96,48 +94,3 @@ function loadCoverUncached(cover: string, paths: SocialCardPaths): Promise<Buffe
96
94
  path.join(paths.cacheDir, "covers", createHash("sha256").update(url.href).digest("hex")),
97
95
  );
98
96
  }
99
-
100
- const preparedCovers = new Map<string, Promise<SharpRawBuffer>>();
101
-
102
- /**
103
- * Crop, flatten and downscale the artwork to the exact panel size. The pixels
104
- * are composited after rasterization instead of being embedded in the SVG: an
105
- * image inside the SVG made Resvg decode and scale it as part of vector
106
- * rasterization, which cost several hundred milliseconds per card.
107
- */
108
- export async function prepareCover(bytes: Buffer): Promise<SharpRawBuffer> {
109
- const sharp = loadSharp();
110
- if (!sharp) throw new Error("sharp is required to crop artwork");
111
- const digest = createHash("sha256").update(bytes).digest("hex");
112
- let pending = preparedCovers.get(digest);
113
- if (!pending) {
114
- pending = sharp(bytes)
115
- .rotate()
116
- .resize({
117
- width: COVER_WIDTH,
118
- height: CARD_HEIGHT,
119
- fit: "cover",
120
- position: "centre",
121
- })
122
- .flatten({ background: "#ffffff" })
123
- .removeAlpha()
124
- .raw()
125
- .toBuffer({ resolveWithObject: true });
126
- preparedCovers.set(digest, pending);
127
- if (preparedCovers.size > 16)
128
- preparedCovers.delete(preparedCovers.keys().next().value ?? "");
129
- }
130
- try {
131
- return await pending;
132
- } catch (error) {
133
- preparedCovers.delete(digest);
134
- throw new Error("cover could not be prepared", { cause: error });
135
- }
136
- }
137
-
138
- /** Only used when sharp is unavailable, so Satori can embed the original bytes. */
139
- export function coverDataUri(bytes: Buffer): string {
140
- const { type } = imageSize(bytes);
141
- const mime = type === "jpg" ? "image/jpeg" : `image/${type ?? "png"}`;
142
- return `data:${mime};base64,${bytes.toString("base64")}`;
143
- }
@@ -4,10 +4,11 @@ import type { SocialCardData } from "../utils/social-image";
4
4
 
5
5
  interface PostData {
6
6
  title: string;
7
- desc: string;
8
7
  date: string;
8
+ abbrlink: string;
9
9
  cover?: string;
10
10
  categories?: string[];
11
+ tags?: string[];
11
12
  }
12
13
 
13
14
  export function createSocialCardData(
@@ -15,13 +16,14 @@ export function createSocialCardData(
15
16
  site: SiteData,
16
17
  author: AuthorData,
17
18
  ): SocialCardData {
19
+ const siteUrl = new URL(site.canonical ?? site.url);
18
20
  return {
19
21
  title: post.title,
20
- description: post.desc,
21
22
  siteTitle: site.title,
22
23
  author: author.name,
23
- host: new URL(site.canonical ?? site.url).host,
24
- lang: site.lang || "zh-CN",
24
+ host: siteUrl.host,
25
+ postUrl: new URL(`/posts/${encodeURIComponent(post.abbrlink)}/`, siteUrl).href,
26
+ tags: post.tags ?? [],
25
27
  date: formatInTimeZone(
26
28
  parseDate(post.date, site.timezone || "Asia/Shanghai"),
27
29
  site.timezone || "Asia/Shanghai",
@@ -1,41 +1,34 @@
1
+ import type { Node } from "@takumi-rs/core";
1
2
  import type { SocialCardData } from "../utils/social-image";
2
3
 
3
4
  export const CARD_WIDTH = 1200;
4
5
  export const CARD_HEIGHT = 630;
5
6
  export const CARD_MARGIN = 72;
6
7
  export const COVER_WIDTH = 440;
7
- export const COVER_LEFT = CARD_WIDTH - COVER_WIDTH;
8
8
  export const CARD_FONT_FAMILY = "Stalux Card";
9
- export const CARD_FONT_WEIGHT = 400;
10
9
 
11
- export interface CardNode {
12
- type: string;
13
- props: Record<string, unknown> & { style?: Record<string, unknown>; children?: unknown };
14
- }
10
+ const MUTED = "#545454";
11
+ const RULE = "rgba(0, 0, 0, 0.16)";
15
12
 
16
- const text = (value: string, style: Record<string, unknown>): CardNode => ({
17
- type: "div",
18
- // `display: block` is what enables `lineClamp` and its ellipsis in Satori.
19
- props: { style: { display: "block", overflow: "hidden", ...style }, children: value },
20
- });
13
+ const text = (value: string, style: Record<string, unknown>): Node =>
14
+ ({ type: "text", text: value, style }) as Node;
21
15
 
22
- const rule = (top: number, width: number): CardNode => ({
23
- type: "div",
24
- props: {
16
+ const rule = (top: number, width: number): Node =>
17
+ ({
18
+ type: "container",
25
19
  style: {
26
20
  position: "absolute",
27
21
  left: CARD_MARGIN,
28
22
  top,
29
23
  width,
30
24
  height: 1,
31
- backgroundColor: "rgba(0, 0, 0, 0.16)",
25
+ backgroundColor: RULE,
32
26
  },
33
- },
34
- });
27
+ }) as Node;
35
28
 
36
- const row = (top: number, width: number, children: CardNode[]): CardNode => ({
37
- type: "div",
38
- props: {
29
+ const row = (top: number, width: number, children: Node[]): Node =>
30
+ ({
31
+ type: "container",
39
32
  style: {
40
33
  position: "absolute",
41
34
  left: CARD_MARGIN,
@@ -46,16 +39,16 @@ const row = (top: number, width: number, children: CardNode[]): CardNode => ({
46
39
  alignItems: "baseline",
47
40
  },
48
41
  children,
49
- },
50
- });
42
+ }) as Node;
51
43
 
52
- function header(data: SocialCardData, textWidth: number): CardNode {
44
+ function header(data: SocialCardData, textWidth: number): Node {
53
45
  return row(50, textWidth, [
54
46
  text(data.siteTitle, {
55
47
  fontSize: 26,
56
48
  lineHeight: 1.2,
57
49
  maxWidth: "62%",
58
50
  lineClamp: 1,
51
+ overflow: "hidden",
59
52
  color: "#000000",
60
53
  }),
61
54
  text(data.category || "", {
@@ -65,32 +58,34 @@ function header(data: SocialCardData, textWidth: number): CardNode {
65
58
  letterSpacing: 3,
66
59
  lineHeight: 1.2,
67
60
  lineClamp: 1,
61
+ overflow: "hidden",
68
62
  color: "#000000",
69
63
  }),
70
64
  ]);
71
65
  }
72
66
 
73
- function footer(data: SocialCardData, textWidth: number): CardNode {
67
+ function footer(data: SocialCardData, textWidth: number): Node {
74
68
  return row(550, textWidth, [
75
69
  text(data.host, { fontSize: 22, lineHeight: 1.3, color: "#000000" }),
76
70
  text(`${data.author} / ${data.date}`, {
77
71
  fontSize: 22,
78
72
  lineHeight: 1.3,
79
73
  textAlign: "right",
80
- color: "#545454",
74
+ color: MUTED,
81
75
  }),
82
76
  ]);
83
77
  }
84
78
 
85
79
  /**
86
80
  * A fixed editorial layout: black type on white, one full-colour artwork panel.
87
- * No accent colour, gradients, or decoration — the title and the image carry it.
81
+ * No accent colour, gradients, or decoration — the title, metadata and image
82
+ * carry it. Every text block is black or neutral gray; only the cover is colour.
88
83
  */
89
- export function socialCardTree(data: SocialCardData, withCover = false, cover?: string): CardNode {
84
+ export function socialCardTree(data: SocialCardData, withCover = false): Node {
90
85
  const textWidth = withCover
91
86
  ? CARD_WIDTH - COVER_WIDTH - CARD_MARGIN * 2
92
87
  : CARD_WIDTH - CARD_MARGIN * 2;
93
- const children: CardNode[] = [
88
+ const children: Node[] = [
94
89
  header(data, textWidth),
95
90
  rule(106, textWidth),
96
91
  text(data.title, {
@@ -101,53 +96,64 @@ export function socialCardTree(data: SocialCardData, withCover = false, cover?:
101
96
  fontSize: 58,
102
97
  lineHeight: 1.2,
103
98
  lineClamp: 3,
99
+ overflow: "hidden",
104
100
  overflowWrap: "anywhere",
105
101
  color: "#000000",
106
102
  }),
107
- text(data.description.replace(/\s+/gu, " ").trim(), {
103
+ ];
104
+ if (data.tags.length > 0)
105
+ children.push(
106
+ text(data.tags.map((tag) => `#${tag}`).join(" · "), {
107
+ position: "absolute",
108
+ left: CARD_MARGIN,
109
+ top: 384,
110
+ width: textWidth,
111
+ fontSize: 23,
112
+ lineHeight: 1.3,
113
+ lineClamp: 1,
114
+ overflow: "hidden",
115
+ color: "#000000",
116
+ }),
117
+ );
118
+ children.push(
119
+ text(data.postUrl.replace(/^https?:\/\//u, ""), {
108
120
  position: "absolute",
109
121
  left: CARD_MARGIN,
110
- top: 398,
122
+ top: 426,
111
123
  width: textWidth,
112
- fontSize: 24,
113
- lineHeight: 1.55,
114
- lineClamp: 2,
115
- overflowWrap: "anywhere",
116
- color: "#545454",
124
+ fontSize: 21,
125
+ lineHeight: 1.3,
126
+ lineClamp: 1,
127
+ overflow: "hidden",
128
+ color: MUTED,
117
129
  }),
118
130
  rule(532, textWidth),
119
131
  footer(data, textWidth),
120
- ];
121
- if (cover)
132
+ );
133
+ if (withCover)
122
134
  children.push({
123
- type: "img",
124
- props: {
125
- src: cover,
126
- style: {
127
- position: "absolute",
128
- right: 0,
129
- top: 0,
130
- width: COVER_WIDTH,
131
- height: CARD_HEIGHT,
132
- objectFit: "cover",
133
- },
134
- },
135
- });
136
- return {
137
- type: "div",
138
- props: {
135
+ type: "image",
136
+ src: "stalux-cover",
139
137
  style: {
140
- width: CARD_WIDTH,
138
+ position: "absolute",
139
+ right: 0,
140
+ top: 0,
141
+ width: COVER_WIDTH,
141
142
  height: CARD_HEIGHT,
142
- position: "relative",
143
- display: "flex",
144
- backgroundColor: "#ffffff",
145
- color: "#000000",
146
- fontFamily: CARD_FONT_FAMILY,
147
- overflow: "hidden",
143
+ objectFit: "cover",
148
144
  },
149
- lang: data.lang,
150
- children,
145
+ } as Node);
146
+ return {
147
+ type: "container",
148
+ style: {
149
+ width: CARD_WIDTH,
150
+ height: CARD_HEIGHT,
151
+ position: "relative",
152
+ backgroundColor: "#ffffff",
153
+ color: "#000000",
154
+ fontFamily: CARD_FONT_FAMILY,
155
+ overflow: "hidden",
151
156
  },
152
- };
157
+ children,
158
+ } as Node;
153
159
  }
@@ -1,106 +1,47 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import { mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import path from "node:path";
4
- import { Resvg } from "@resvg/resvg-js";
5
- import satori from "satori";
4
+ import { Renderer } from "@takumi-rs/core";
6
5
  import { type SocialCardData, socialCardFormat } from "../utils/social-image";
7
- import { loadSharp, type SharpPipeline, type SharpRawBuffer } from "./sharp-loader";
8
- import { coverDataUri, loadCover, prepareCover, type SocialCardPaths } from "./social-card-cover";
9
- import {
10
- CARD_FONT_FAMILY,
11
- CARD_FONT_WEIGHT,
12
- CARD_HEIGHT,
13
- CARD_WIDTH,
14
- COVER_LEFT,
15
- socialCardTree,
16
- } from "./social-card-layout";
6
+ import { loadCover, type SocialCardPaths } from "./social-card-cover";
7
+ import { CARD_FONT_FAMILY, CARD_HEIGHT, CARD_WIDTH, socialCardTree } from "./social-card-layout";
17
8
 
18
9
  export type { SocialCardPaths } from "./social-card-cover";
19
10
  export { localCoverPath } from "./social-card-cover";
20
11
  export { socialCardTree } from "./social-card-layout";
21
12
 
22
- /** Bump when the layout changes so every cached render is replaced. */
23
- const TEMPLATE = "stalux-mono-v1";
13
+ /** Bump when the layout or renderer changes so every cached render is replaced. */
14
+ const TEMPLATE = "stalux-mono-takumi-v2";
15
+ const JPEG_QUALITY = 60;
24
16
 
25
- type SatoriInput = Parameters<typeof satori>[0];
17
+ const renderers = new Map<string, Promise<Renderer>>();
26
18
 
27
- function encodeWithSharp(
28
- pipeline: SharpPipeline,
29
- format: "jpg" | "png",
30
- panel?: SharpRawBuffer,
31
- ): Promise<Buffer> {
32
- if (format === "jpg") {
33
- // The artwork keeps its colour. Without artwork the whole frame is
34
- // black and white, so the smaller true grayscale JPEG is used.
35
- const composed = panel
36
- ? pipeline.composite([
37
- {
38
- input: panel.data,
39
- raw: {
40
- width: panel.info.width,
41
- height: panel.info.height,
42
- channels: panel.info.channels,
43
- },
44
- left: COVER_LEFT,
45
- top: 0,
46
- },
47
- ])
48
- : pipeline.greyscale().toColourspace("b-w");
49
- return composed.jpeg({ quality: 80, mozjpeg: true }).toBuffer();
50
- }
51
- return pipeline
52
- .greyscale()
53
- .png({ compressionLevel: 9, palette: true, colours: 256, effort: 3 })
54
- .toBuffer();
55
- }
56
-
57
- /**
58
- * Rasterize with Resvg, then let Sharp compose and encode in a single pass.
59
- * System-font discovery is disabled because Satori already converted every
60
- * glyph to a path; leaving it on made each Resvg instance scan all installed
61
- * fonts, costing about 500ms per card.
62
- */
63
- async function renderCardBytes(
64
- svg: string,
65
- format: "jpg" | "png",
66
- panel?: SharpRawBuffer,
67
- ): Promise<Buffer> {
68
- const png = new Resvg(svg, {
69
- fitTo: { mode: "width", value: CARD_WIDTH },
70
- background: "#ffffff",
71
- font: { loadSystemFonts: false },
72
- })
73
- .render()
74
- .asPng();
75
- const sharp = loadSharp();
76
- if (!sharp) {
77
- if (format === "jpg") throw new Error("JPEG encoding requires sharp");
78
- return png;
79
- }
80
- try {
81
- return await encodeWithSharp(sharp(png), format, panel);
82
- } catch (error) {
83
- if (format === "jpg") throw error;
84
- return png;
85
- }
86
- }
87
-
88
- const fonts = new Map<string, Promise<Buffer>>();
89
- function readFont(fontPath: string): Promise<Buffer> {
90
- let pending = fonts.get(fontPath);
19
+ function getRenderer(fontPath: string): Promise<Renderer> {
20
+ let pending = renderers.get(fontPath);
91
21
  if (!pending) {
92
- pending = readFile(fontPath);
93
- fonts.set(fontPath, pending);
22
+ pending = (async () => {
23
+ const renderer = new Renderer({ cacheMaxBytes: 32 * 1024 * 1024 });
24
+ await renderer.registerFont({
25
+ name: CARD_FONT_FAMILY,
26
+ data: await readFile(fontPath),
27
+ });
28
+ return renderer;
29
+ })();
30
+ renderers.set(fontPath, pending);
94
31
  }
95
32
  return pending;
96
33
  }
97
34
 
98
- let fontDigestPromise: Promise<string> | undefined;
35
+ const fontDigests = new Map<string, Promise<string>>();
99
36
  function getFontDigest(fontPath: string): Promise<string> {
100
- fontDigestPromise ??= readFile(fontPath).then((bytes) =>
101
- createHash("sha256").update(bytes).digest("hex"),
102
- );
103
- return fontDigestPromise;
37
+ let pending = fontDigests.get(fontPath);
38
+ if (!pending) {
39
+ pending = readFile(fontPath).then((bytes) =>
40
+ createHash("sha256").update(bytes).digest("hex"),
41
+ );
42
+ fontDigests.set(fontPath, pending);
43
+ }
44
+ return pending;
104
45
  }
105
46
 
106
47
  export async function renderSocialCard(
@@ -109,7 +50,6 @@ export async function renderSocialCard(
109
50
  warn: (message: string) => void,
110
51
  ): Promise<Buffer> {
111
52
  const format = socialCardFormat(data);
112
- const sharpAvailable = Boolean(loadSharp());
113
53
  let coverBytes: Buffer | undefined;
114
54
  if (data.cover) {
115
55
  try {
@@ -120,67 +60,38 @@ export async function renderSocialCard(
120
60
  }
121
61
  // Content-only key: identical card data + identical font + identical cover
122
62
  // always reuses the cached render, whatever the filesystem timestamps say.
123
- // It is checked before cropping artwork so a warm build does no image work.
63
+ // It is checked before any raster work, so a warm build only reads bytes.
124
64
  const fontDigest = await getFontDigest(paths.fontPath);
125
65
  const key = createHash("sha256")
126
- .update(
127
- JSON.stringify([TEMPLATE, format, data, fontDigest, sharpAvailable ? "panel" : "flat"]),
128
- )
66
+ .update(JSON.stringify([TEMPLATE, format, data, fontDigest]))
129
67
  .update(coverBytes ?? "")
130
68
  .digest("hex");
131
69
  const cacheFile = path.join(paths.cacheDir, `${key}.${format}`);
132
70
  const hit = await readFile(cacheFile).catch(() => undefined);
133
71
  if (hit) return hit;
134
72
 
135
- let panel: SharpRawBuffer | undefined;
136
- let inlineCover: string | undefined;
137
- if (coverBytes) {
138
- if (sharpAvailable) {
139
- try {
140
- panel = await prepareCover(coverBytes);
141
- } catch {
142
- warn(`分享封面不可用,已回退标题卡片:${data.title}`);
143
- }
144
- } else {
145
- // Without sharp there is no compositor, so the original bytes go
146
- // through Satori instead (PNG/JPEG only, and no palette encoding).
147
- inlineCover = coverDataUri(coverBytes);
148
- }
149
- }
150
-
151
- const withCover = Boolean(panel || inlineCover);
152
- const renderTree = async (hasCover: boolean) => {
153
- const svg = await satori(
154
- socialCardTree(
155
- data,
156
- hasCover,
157
- hasCover ? inlineCover : undefined,
158
- ) as unknown as SatoriInput,
159
- {
160
- width: CARD_WIDTH,
161
- height: CARD_HEIGHT,
162
- fonts: [
163
- {
164
- name: CARD_FONT_FAMILY,
165
- data: await readFont(paths.fontPath),
166
- weight: CARD_FONT_WEIGHT,
167
- style: "normal",
168
- },
169
- ],
170
- },
171
- );
172
- return renderCardBytes(svg, format, hasCover ? panel : undefined);
173
- };
73
+ const renderer = await getRenderer(paths.fontPath);
74
+ const render = (withCover: boolean) =>
75
+ renderer.render(socialCardTree(data, withCover), {
76
+ width: CARD_WIDTH,
77
+ height: CARD_HEIGHT,
78
+ format: format === "jpg" ? "jpeg" : "png",
79
+ quality: JPEG_QUALITY,
80
+ images: withCover && coverBytes ? [{ src: "stalux-cover", data: coverBytes }] : [],
81
+ });
174
82
  let bytes: Buffer;
83
+ let cacheable = true;
175
84
  try {
176
- bytes = await renderTree(withCover);
85
+ bytes = await render(Boolean(coverBytes));
177
86
  } catch (error) {
178
- if (!withCover) throw error;
87
+ if (!coverBytes) throw error;
179
88
  warn(`分享封面解码失败,已回退标题卡片:${data.title}`);
180
89
  // Do not cache the fallback under the artwork key: a later build should
181
90
  // retry the cover instead of inheriting a short-lived decode failure.
182
- return renderTree(false);
91
+ bytes = await render(false);
92
+ cacheable = false;
183
93
  }
94
+ if (!cacheable) return bytes;
184
95
  await mkdir(paths.cacheDir, { recursive: true });
185
96
  await writeFile(cacheFile, bytes);
186
97
  return bytes;
@@ -1,13 +1,12 @@
1
1
  import { createHash } from "node:crypto";
2
- import { loadSharp } from "../internal/sharp-loader";
3
2
 
4
3
  export interface SocialCardData {
5
4
  title: string;
6
- description: string;
7
5
  siteTitle: string;
8
6
  author: string;
9
7
  host: string;
10
- lang: string;
8
+ postUrl: string;
9
+ tags: string[];
11
10
  date: string;
12
11
  category: string;
13
12
  cover?: string;
@@ -37,20 +36,15 @@ export function isSocialImageUrl(value: string): boolean {
37
36
  }
38
37
  }
39
38
 
40
- /**
41
- * Cards with artwork are photographic, so JPEG saves most of the bytes and is
42
- * accepted by every crawler. Plain cards stay PNG: flat type compresses far
43
- * better there. Without `sharp` neither palette PNG nor JPEG is available, so
44
- * the card falls back to a plain PNG.
45
- */
39
+ /** Artwork cards are photographic, so JPEG saves bytes; plain cards stay PNG. */
46
40
  export function socialCardFormat(data: Pick<SocialCardData, "cover">): SocialCardFormat {
47
- return data.cover && loadSharp() ? "jpg" : "png";
41
+ return data.cover ? "jpg" : "png";
48
42
  }
49
43
 
50
44
  export function socialCardPath(data: SocialCardData, base = "/"): string {
51
45
  // Bump the template revision when layout changes. Metadata changes get a fresh URL.
52
46
  const hash = createHash("sha256")
53
- .update(JSON.stringify(["stalux-mono-v1", data]))
47
+ .update(JSON.stringify(["stalux-mono-v2", data]))
54
48
  .digest("hex")
55
49
  .slice(0, 20);
56
50
  return `${base.replace(/\/$/u, "")}/og/${hash}.${socialCardFormat(data)}`;
@@ -1,45 +0,0 @@
1
- import { createRequire } from "node:module";
2
-
3
- export interface SharpRawBuffer {
4
- data: Buffer;
5
- info: { width: number; height: number; channels: number };
6
- }
7
-
8
- export interface SharpPipeline {
9
- rotate: () => SharpPipeline;
10
- resize: (options: Record<string, unknown>) => SharpPipeline;
11
- flatten: (options: Record<string, unknown>) => SharpPipeline;
12
- removeAlpha: () => SharpPipeline;
13
- composite: (images: Record<string, unknown>[]) => SharpPipeline;
14
- greyscale: () => SharpPipeline;
15
- toColourspace: (colourspace: string) => SharpPipeline;
16
- raw: () => SharpPipeline;
17
- jpeg: (options: Record<string, unknown>) => SharpPipeline;
18
- png: (options: Record<string, unknown>) => SharpPipeline;
19
- toBuffer(): Promise<Buffer>;
20
- toBuffer(options: { resolveWithObject: true }): Promise<SharpRawBuffer>;
21
- }
22
-
23
- export type SharpFactory = (input: Buffer) => SharpPipeline;
24
-
25
- let cached: { factory: SharpFactory | undefined } | undefined;
26
-
27
- /**
28
- * `sharp` is optional: it ships with Astro and is a dev dependency of the theme,
29
- * but a project using another image service must still be able to build. It is
30
- * loaded with `require` because the Vite module runner may already be closed and
31
- * because a bare dynamic import would make the bundler resolve it eagerly.
32
- */
33
- export function loadSharp(): SharpFactory | undefined {
34
- if (cached) return cached.factory;
35
- let factory: SharpFactory | undefined;
36
- try {
37
- const loaded = createRequire(import.meta.url)("sharp") as { default?: unknown };
38
- const candidate = (loaded.default ?? loaded) as unknown;
39
- if (typeof candidate === "function") factory = candidate as SharpFactory;
40
- } catch {
41
- factory = undefined;
42
- }
43
- cached = { factory };
44
- return factory;
45
- }