storeshot 0.1.0 → 0.2.0

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.
@@ -0,0 +1,517 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ DEFAULT_TEXT_LINE_HEIGHT_RATIO,
4
+ MOCKUP_BUNDLE_FILENAME,
5
+ builtInArtworkById,
6
+ parseMockupBundleManifest,
7
+ resolveMockupBundle,
8
+ resolvePackagePublicDirectory
9
+ } from "./chunk-I7JOE7XJ.js";
10
+
11
+ // src/node-renderer.ts
12
+ import { readFile, readdir, mkdir as mkdir2, rm as rm2, writeFile as writeFile2 } from "fs/promises";
13
+ import path2 from "path";
14
+ import { createCanvas, loadImage } from "canvas";
15
+ import {
16
+ FabricImage,
17
+ Group,
18
+ Rect,
19
+ StaticCanvas,
20
+ Textbox,
21
+ filters,
22
+ loadSVGFromString,
23
+ util
24
+ } from "fabric/node";
25
+
26
+ // src/node-fonts.ts
27
+ import { createHash, randomUUID } from "crypto";
28
+ import { access, mkdir, rename, rm, writeFile } from "fs/promises";
29
+ import { tmpdir } from "os";
30
+ import path from "path";
31
+ import { registerFont } from "canvas";
32
+ var BUNNY_FONT_ORIGIN = "https://fonts.bunny.net";
33
+ var FONT_CACHE_DIRECTORY = path.join(tmpdir(), "storeshot-fonts-v1");
34
+ var LOCAL_RENDER_FONTS = /* @__PURE__ */ new Set(["Geist Variable", "Arial", "Georgia", "Times New Roman"]);
35
+ var MAX_FONT_BYTES = 10 * 1024 * 1024;
36
+ var FONT_WEIGHTS = [100, 200, 300, 400, 500, 600, 700, 800, 900];
37
+ var bundledFontsRegistered = false;
38
+ async function registerScreenshotSetFonts(packageRoot, sets) {
39
+ registerBundledFonts(packageRoot);
40
+ const dependencies = {
41
+ cacheDirectory: FONT_CACHE_DIRECTORY,
42
+ fetch,
43
+ register: registerFont
44
+ };
45
+ await Promise.all(collectFontRequests(sets).map((request) => loadBunnyFont(request, dependencies)));
46
+ }
47
+ async function loadBunnyFont(request, dependencies) {
48
+ const stylesheetUrl = new URL("/css", BUNNY_FONT_ORIGIN);
49
+ stylesheetUrl.searchParams.set("family", `${request.family}:${FONT_WEIGHTS.join(",")}`);
50
+ stylesheetUrl.searchParams.set("display", "swap");
51
+ const stylesheet = await fetchText(stylesheetUrl, dependencies.fetch, `font stylesheet for ${request.family}`);
52
+ const faces = selectWoffFaces(stylesheet, request);
53
+ if (faces.length === 0) {
54
+ throw new Error(`Bunny Fonts did not provide a usable font for ${request.family}`);
55
+ }
56
+ await mkdir(dependencies.cacheDirectory, { recursive: true });
57
+ for (const { url, weight } of faces) {
58
+ let filename = await cacheFont(url, dependencies);
59
+ try {
60
+ dependencies.register(filename, { family: request.family, style: "normal", weight: String(weight) });
61
+ } catch {
62
+ await rm(filename, { force: true });
63
+ filename = await cacheFont(url, dependencies);
64
+ dependencies.register(filename, { family: request.family, style: "normal", weight: String(weight) });
65
+ }
66
+ }
67
+ }
68
+ function selectWoffFaces(stylesheet, request) {
69
+ const candidates = [];
70
+ for (const match of stylesheet.matchAll(/@font-face\s*\{([\s\S]*?)\}/giu)) {
71
+ const block = match[1];
72
+ const family = cssValue(block, "font-family").replace(/^(['"])(.*)\1$/u, "$2");
73
+ const weight = Number(cssValue(block, "font-weight"));
74
+ const style = cssValue(block, "font-style");
75
+ if (family !== request.family || !isFontWeight(weight) || style !== "normal") continue;
76
+ const range = cssValue(block, "unicode-range");
77
+ if (range && !textIntersectsUnicodeRange(request.text, range)) continue;
78
+ const source = cssValue(block, "src");
79
+ const url = source.match(/url\((?:['"]?)(https:\/\/fonts\.bunny\.net\/[^)'"]+\.woff(?:\?[^)'"]*)?)(?:['"]?)\)\s*format\((?:['"])woff(?:['"])\)/iu)?.[1];
80
+ if (url) candidates.push({ url, weight });
81
+ }
82
+ const availableWeights = [...new Set(candidates.map((face) => face.weight))];
83
+ if (availableWeights.length === 0) return [];
84
+ const closestWeight = availableWeights.reduce((closest, weight) => Math.abs(weight - request.weight) < Math.abs(closest - request.weight) ? weight : closest, availableWeights[0]);
85
+ return candidates.filter((face) => face.weight === closestWeight);
86
+ }
87
+ function collectFontRequests(sets) {
88
+ const requests = /* @__PURE__ */ new Map();
89
+ for (const set of sets) {
90
+ for (const area of set.areas) {
91
+ for (const element of area.elements) {
92
+ if (element.type !== "text" || LOCAL_RENDER_FONTS.has(element.fontFamily) || element.text.length === 0) continue;
93
+ const key = `${element.fontFamily}\0${element.fontWeight}`;
94
+ const existing = requests.get(key);
95
+ if (existing) existing.text += `
96
+ ${element.text}`;
97
+ else requests.set(key, { family: element.fontFamily, text: element.text, weight: element.fontWeight });
98
+ }
99
+ }
100
+ }
101
+ return [...requests.values()];
102
+ }
103
+ function registerBundledFonts(packageRoot) {
104
+ if (bundledFontsRegistered) return;
105
+ try {
106
+ registerFont(path.join(packageRoot, "fonts/Geist-Variable.ttf"), { family: "Geist Variable" });
107
+ } catch (error) {
108
+ if (error.code !== "ENOENT") throw error;
109
+ }
110
+ bundledFontsRegistered = true;
111
+ }
112
+ async function cacheFont(urlValue, dependencies) {
113
+ const url = new URL(urlValue);
114
+ if (url.protocol !== "https:" || url.hostname !== "fonts.bunny.net") {
115
+ throw new Error(`Refusing font URL outside ${BUNNY_FONT_ORIGIN}`);
116
+ }
117
+ const digest = createHash("sha256").update(url.href).digest("hex");
118
+ const filename = path.join(dependencies.cacheDirectory, `${digest}.woff`);
119
+ if (await fileExists(filename)) return filename;
120
+ const response = await dependencies.fetch(url);
121
+ if (!response.ok) throw new Error(`Bunny Fonts returned ${response.status} for ${url.pathname}`);
122
+ const declaredSize = Number(response.headers.get("content-length"));
123
+ if (Number.isFinite(declaredSize) && declaredSize > MAX_FONT_BYTES) throw new Error(`Bunny font exceeds ${MAX_FONT_BYTES} bytes`);
124
+ const contents = Buffer.from(await response.arrayBuffer());
125
+ if (contents.length === 0 || contents.length > MAX_FONT_BYTES) throw new Error(`Invalid Bunny font size: ${contents.length} bytes`);
126
+ const temporary = `${filename}.${process.pid}.${randomUUID()}.tmp`;
127
+ await writeFile(temporary, contents, { flag: "wx" });
128
+ try {
129
+ await rename(temporary, filename);
130
+ } catch (error) {
131
+ if (!await fileExists(filename)) throw error;
132
+ await rm(temporary, { force: true });
133
+ }
134
+ return filename;
135
+ }
136
+ async function fetchText(url, request, description) {
137
+ const response = await request(url, { headers: { accept: "text/css" } });
138
+ if (!response.ok) throw new Error(`Could not load ${description}: Bunny Fonts returned ${response.status}`);
139
+ return response.text();
140
+ }
141
+ function cssValue(block, property) {
142
+ const match = block.match(new RegExp(`(?:^|\\n)\\s*${property}\\s*:\\s*([^;]+)`, "iu"));
143
+ return match?.[1].trim() ?? "";
144
+ }
145
+ function textIntersectsUnicodeRange(text, value) {
146
+ const ranges = value.split(",").flatMap(parseUnicodeRange);
147
+ if (ranges.length === 0) return true;
148
+ return [...text].some((character) => {
149
+ const codePoint = character.codePointAt(0);
150
+ return codePoint !== void 0 && ranges.some((range) => codePoint >= range.start && codePoint <= range.end);
151
+ });
152
+ }
153
+ function parseUnicodeRange(value) {
154
+ const normalized = value.trim().replace(/^U\+/iu, "");
155
+ if (/^[0-9A-F]+-[0-9A-F]+$/iu.test(normalized)) {
156
+ const [start, end] = normalized.split("-").map((part) => Number.parseInt(part, 16));
157
+ return [{ start, end }];
158
+ }
159
+ if (/^[0-9A-F?]+$/iu.test(normalized)) {
160
+ return [{
161
+ start: Number.parseInt(normalized.replaceAll("?", "0"), 16),
162
+ end: Number.parseInt(normalized.replaceAll("?", "F"), 16)
163
+ }];
164
+ }
165
+ return [];
166
+ }
167
+ function isFontWeight(value) {
168
+ return Number.isInteger(value) && value >= 100 && value <= 900 && value % 100 === 0;
169
+ }
170
+ async function fileExists(filename) {
171
+ try {
172
+ await access(filename);
173
+ return true;
174
+ } catch {
175
+ return false;
176
+ }
177
+ }
178
+
179
+ // src/node-renderer.ts
180
+ async function renderScreenshotSets(options) {
181
+ if (!Number.isFinite(options.scale) || options.scale <= 0 || options.scale > 1) {
182
+ throw new Error("Render scale must be greater than 0 and at most 1");
183
+ }
184
+ if (options.area && options.sets.length !== 1) throw new Error("--area can only be used when rendering one set");
185
+ const selections = options.sets.map((set) => ({ set, areas: selectAreas(set, options.area) }));
186
+ const selectedSets = selections.map(({ set, areas }) => ({
187
+ ...set,
188
+ areas: areas.map(({ area }) => area)
189
+ }));
190
+ await registerScreenshotSetFonts(options.packageRoot, selectedSets);
191
+ const resources = await loadRenderResources(options.store, options.packageRoot, requiredResources(selectedSets));
192
+ const outputDirectory = path2.resolve(options.outputDirectory);
193
+ const files = [];
194
+ for (const { set, areas: selectedAreas } of selections) {
195
+ const setDirectory = path2.join(outputDirectory, set.id);
196
+ if (options.clean) await rm2(setDirectory, { recursive: true, force: true });
197
+ await mkdir2(setDirectory, { recursive: true });
198
+ for (const { area, index } of selectedAreas) {
199
+ const outputPath = path2.join(setDirectory, `${String(index + 1).padStart(2, "0")}-${safeFileNamePart(area.name, `screenshot-${index + 1}`)}.png`);
200
+ const data = await renderArea(area, set, resources, options.scale);
201
+ await writeFile2(outputPath, data);
202
+ files.push({
203
+ areaId: area.id,
204
+ height: Math.round(set.canvas.height * options.scale),
205
+ path: outputPath,
206
+ setId: set.id,
207
+ width: Math.round(set.canvas.width * options.scale)
208
+ });
209
+ }
210
+ }
211
+ return { files, outputDirectory };
212
+ }
213
+ async function renderArea(area, set, resources, scale) {
214
+ const width = Math.max(1, Math.round(set.canvas.width * scale));
215
+ const height = Math.max(1, Math.round(set.canvas.height * scale));
216
+ const canvas = new StaticCanvas(void 0, {
217
+ width,
218
+ height,
219
+ backgroundColor: area.background,
220
+ renderOnAddRemove: false
221
+ });
222
+ try {
223
+ for (const element of area.elements) {
224
+ const object = await createFabricObject(element, resources);
225
+ applyCanvasElement(object, element, scale);
226
+ object.set({ evented: false, selectable: false });
227
+ canvas.add(object);
228
+ }
229
+ canvas.renderAll();
230
+ return canvas.getNodeCanvas().toBuffer("image/png");
231
+ } finally {
232
+ canvas.dispose();
233
+ }
234
+ }
235
+ async function createFabricObject(element, resources) {
236
+ if (element.type === "text") return new Textbox(element.text, { editable: false, lockScalingFlip: true, minWidth: 24 });
237
+ if (element.type === "shape") return new Rect({ lockScalingFlip: true });
238
+ if (element.type === "mockup") {
239
+ const screenshot = resources.assetSources.get(element.assetId);
240
+ const mockup = resources.mockups.get(element.mockupId);
241
+ if (!screenshot) throw new Error(`Missing asset ${element.assetId}`);
242
+ if (!mockup) throw new Error(`Missing mockup ${element.mockupId}`);
243
+ return renderDeviceMockup(mockup, screenshot);
244
+ }
245
+ const source = element.source.kind === "builtin" ? await builtInArtworkSource(element.source.id, resources.publicDirectory) : resources.assetSources.get(element.source.assetId);
246
+ if (!source) throw new Error(`Missing image source for element ${element.id}`);
247
+ let object;
248
+ if (source.startsWith("data:image/svg+xml")) {
249
+ const svg = Buffer.from(source.slice(source.indexOf(",") + 1), "base64").toString("utf8");
250
+ const loaded = await loadSVGFromString(svg);
251
+ const objects = loaded.objects.filter((candidate) => candidate !== null);
252
+ if (objects.length === 0) throw new Error(`SVG source for element ${element.id} is empty`);
253
+ object = util.groupSVGElements(objects, loaded.options);
254
+ object.set({ lockScalingFlip: true });
255
+ } else {
256
+ object = await FabricImage.fromURL(source, {}, { imageSmoothing: true, lockScalingFlip: true });
257
+ }
258
+ if (element.fill) applyGraphicColor(object, element.fill);
259
+ return object;
260
+ }
261
+ function applyCanvasElement(object, element, scale) {
262
+ object.set({
263
+ angle: element.rotation,
264
+ left: element.x * scale,
265
+ opacity: element.opacity,
266
+ originX: "left",
267
+ originY: "top",
268
+ top: element.y * scale
269
+ });
270
+ if (element.type === "text" && object instanceof Textbox) {
271
+ object.set({
272
+ fill: element.color,
273
+ fontFamily: element.fontFamily,
274
+ fontSize: element.fontSize * scale,
275
+ fontWeight: element.fontWeight,
276
+ lineHeight: element.lineHeight === void 0 ? DEFAULT_TEXT_LINE_HEIGHT_RATIO : element.lineHeight / element.fontSize,
277
+ scaleX: 1,
278
+ scaleY: 1,
279
+ strokeWidth: 0,
280
+ text: element.text,
281
+ textAlign: element.textAlign,
282
+ width: Math.max(24, element.width * scale)
283
+ });
284
+ object.initDimensions();
285
+ } else if (element.type === "shape" && object instanceof Rect) {
286
+ object.set({
287
+ fill: element.fill,
288
+ height: element.height * scale,
289
+ rx: element.cornerRadius * scale,
290
+ ry: element.cornerRadius * scale,
291
+ scaleX: 1,
292
+ scaleY: 1,
293
+ strokeWidth: 0,
294
+ width: element.width * scale
295
+ });
296
+ } else {
297
+ object.set({
298
+ scaleX: element.width * scale / Math.max(1, object.width),
299
+ scaleY: element.height * scale / Math.max(1, object.height)
300
+ });
301
+ }
302
+ object.setCoords();
303
+ }
304
+ async function loadRenderResources(store, packageRoot, required) {
305
+ const [project, publicDirectory] = await Promise.all([
306
+ store.readProject(),
307
+ resolvePackagePublicDirectory(packageRoot)
308
+ ]);
309
+ const assetSources = /* @__PURE__ */ new Map();
310
+ await Promise.all(Object.values(project.assets).flat().filter((asset) => required.assets.has(asset.id)).map(async (asset) => {
311
+ const filename = await store.resolveExistingAsset(asset.category, asset.name);
312
+ assetSources.set(asset.id, dataUrl(filename, await readFile(filename)));
313
+ }));
314
+ const mockups = /* @__PURE__ */ new Map();
315
+ await loadMockupBundle(
316
+ path2.join(publicDirectory, "mockup-bundles/frameup-free"),
317
+ "built-in",
318
+ mockups,
319
+ required.mockups
320
+ );
321
+ const entries = await readdir(store.mockupBundlesPath, { withFileTypes: true });
322
+ for (const entry of entries) {
323
+ if (!entry.isDirectory() || entry.name.startsWith(".")) continue;
324
+ if (![...required.mockups].some((mockupId) => mockupId.startsWith(`${entry.name}/`))) continue;
325
+ await loadMockupBundle(path2.join(store.mockupBundlesPath, entry.name), "project", mockups, required.mockups, store);
326
+ }
327
+ return { assetSources, mockups, publicDirectory };
328
+ }
329
+ async function loadMockupBundle(directory, origin, target, requiredMockups, store) {
330
+ const manifest = parseMockupBundleManifest(JSON.parse(await readFile(path2.join(directory, MOCKUP_BUNDLE_FILENAME), "utf8")));
331
+ const catalog = resolveMockupBundle(manifest, "", origin);
332
+ for (const mockup of catalog.mockups) {
333
+ if (!requiredMockups.has(mockup.id)) continue;
334
+ const entry = manifest.mockups.find((candidate) => `${manifest.id}/${candidate.id}` === mockup.id);
335
+ if (!entry) continue;
336
+ const framePath = store ? await store.resolveExistingMockupBundleFile(manifest.id, entry.frame) : path2.join(directory, entry.frame);
337
+ target.set(mockup.id, { ...mockup, frameUrl: dataUrl(framePath, await readFile(framePath)) });
338
+ }
339
+ }
340
+ async function builtInArtworkSource(id, publicDirectory) {
341
+ const artwork = builtInArtworkById(id);
342
+ if (!artwork) return void 0;
343
+ const filename = path2.join(publicDirectory, artwork.url.replace(/^\//, ""));
344
+ return dataUrl(filename, await readFile(filename));
345
+ }
346
+ async function renderDeviceMockup(mockup, screenshotUrl) {
347
+ const [frame, screenshot] = await Promise.all([
348
+ loadImage(mockup.frameUrl),
349
+ loadImage(screenshotUrl)
350
+ ]);
351
+ const canvas = createCanvas(mockup.width, mockup.height);
352
+ const context = canvas.getContext("2d");
353
+ context.imageSmoothingEnabled = true;
354
+ if (mockup.screen.kind === "rect") {
355
+ const screen = mockup.screen;
356
+ context.save();
357
+ context.beginPath();
358
+ context.roundRect(screen.x, screen.y, screen.width, screen.height, screen.cornerRadius);
359
+ context.clip();
360
+ context.drawImage(screenshot, screen.x, screen.y, screen.width, screen.height);
361
+ context.restore();
362
+ } else {
363
+ const masked = maskRoundedScreenshot(screenshot, mockup.screen.sourceCornerRadius);
364
+ drawProjectiveImage(context, masked, mockup.screen.transform, mockup.width, mockup.height);
365
+ }
366
+ context.drawImage(frame, 0, 0, mockup.width, mockup.height);
367
+ return new FabricImage(canvas, { imageSmoothing: true, lockScalingFlip: true });
368
+ }
369
+ function maskRoundedScreenshot(image, radius) {
370
+ const canvas = createCanvas(image.width, image.height);
371
+ const context = canvas.getContext("2d");
372
+ context.imageSmoothingEnabled = true;
373
+ context.beginPath();
374
+ roundedRectPath(context, canvas.width, canvas.height, canvas.width * radius.x, canvas.height * radius.y);
375
+ context.clip();
376
+ context.drawImage(image, 0, 0, canvas.width, canvas.height);
377
+ return canvas;
378
+ }
379
+ function roundedRectPath(context, width, height, radiusX, radiusY) {
380
+ const rx = Math.min(Math.max(0, radiusX), width / 2);
381
+ const ry = Math.min(Math.max(0, radiusY), height / 2);
382
+ if (rx === 0 || ry === 0) {
383
+ context.rect(0, 0, width, height);
384
+ return;
385
+ }
386
+ context.moveTo(rx, 0);
387
+ context.lineTo(width - rx, 0);
388
+ context.ellipse(width - rx, ry, rx, ry, 0, -Math.PI / 2, 0);
389
+ context.lineTo(width, height - ry);
390
+ context.ellipse(width - rx, height - ry, rx, ry, 0, 0, Math.PI / 2);
391
+ context.lineTo(rx, height);
392
+ context.ellipse(rx, height - ry, rx, ry, 0, Math.PI / 2, Math.PI);
393
+ context.lineTo(0, ry);
394
+ context.ellipse(rx, ry, rx, ry, 0, Math.PI, Math.PI * 1.5);
395
+ context.closePath();
396
+ }
397
+ function requiredResources(sets) {
398
+ const assets = /* @__PURE__ */ new Set();
399
+ const mockups = /* @__PURE__ */ new Set();
400
+ for (const set of sets) {
401
+ for (const area of set.areas) {
402
+ for (const element of area.elements) {
403
+ if (element.type === "mockup") {
404
+ assets.add(element.assetId);
405
+ mockups.add(element.mockupId);
406
+ } else if (element.type === "image" && element.source.kind === "asset") {
407
+ assets.add(element.source.assetId);
408
+ }
409
+ }
410
+ }
411
+ }
412
+ return { assets, mockups };
413
+ }
414
+ function drawProjectiveImage(context, image, transform, frameWidth, frameHeight) {
415
+ const columns = Math.max(12, Math.ceil(frameWidth / 64));
416
+ const rows = Math.max(20, Math.ceil(frameHeight / 64));
417
+ for (let row = 0; row < rows; row += 1) {
418
+ for (let column = 0; column < columns; column += 1) {
419
+ const u0 = column / columns;
420
+ const u1 = (column + 1) / columns;
421
+ const v0 = row / rows;
422
+ const v1 = (row + 1) / rows;
423
+ const destination00 = projectPoint(transform, frameWidth, frameHeight, u0, v0);
424
+ const destination10 = projectPoint(transform, frameWidth, frameHeight, u1, v0);
425
+ const destination11 = projectPoint(transform, frameWidth, frameHeight, u1, v1);
426
+ const destination01 = projectPoint(transform, frameWidth, frameHeight, u0, v1);
427
+ const source00 = sourcePoint(image, u0, v0);
428
+ const source10 = sourcePoint(image, u1, v0);
429
+ const source11 = sourcePoint(image, u1, v1);
430
+ const source01 = sourcePoint(image, u0, v1);
431
+ drawImageTriangle(context, image, [source00, source10, source11], [destination00, destination10, destination11]);
432
+ drawImageTriangle(context, image, [source00, source11, source01], [destination00, destination11, destination01]);
433
+ }
434
+ }
435
+ }
436
+ function drawImageTriangle(context, image, source, destination) {
437
+ const [source0, source1, source2] = source;
438
+ const [destination0, destination1, destination2] = destination;
439
+ const denominator = source0.x * (source1.y - source2.y) + source1.x * (source2.y - source0.y) + source2.x * (source0.y - source1.y);
440
+ if (Math.abs(denominator) < Number.EPSILON) return;
441
+ const a = (destination0.x * (source1.y - source2.y) + destination1.x * (source2.y - source0.y) + destination2.x * (source0.y - source1.y)) / denominator;
442
+ const b = (destination0.y * (source1.y - source2.y) + destination1.y * (source2.y - source0.y) + destination2.y * (source0.y - source1.y)) / denominator;
443
+ const c = (destination0.x * (source2.x - source1.x) + destination1.x * (source0.x - source2.x) + destination2.x * (source1.x - source0.x)) / denominator;
444
+ const d = (destination0.y * (source2.x - source1.x) + destination1.y * (source0.x - source2.x) + destination2.y * (source1.x - source0.x)) / denominator;
445
+ const e = (destination0.x * (source1.x * source2.y - source2.x * source1.y) + destination1.x * (source2.x * source0.y - source0.x * source2.y) + destination2.x * (source0.x * source1.y - source1.x * source0.y)) / denominator;
446
+ const f = (destination0.y * (source1.x * source2.y - source2.x * source1.y) + destination1.y * (source2.x * source0.y - source0.x * source2.y) + destination2.y * (source0.x * source1.y - source1.x * source0.y)) / denominator;
447
+ context.save();
448
+ const expanded = expandTriangle(destination, 0.75);
449
+ context.beginPath();
450
+ context.moveTo(expanded[0].x, expanded[0].y);
451
+ context.lineTo(expanded[1].x, expanded[1].y);
452
+ context.lineTo(expanded[2].x, expanded[2].y);
453
+ context.closePath();
454
+ context.clip();
455
+ context.setTransform(a, b, c, d, e, f);
456
+ context.drawImage(image, 0, 0);
457
+ context.restore();
458
+ }
459
+ function projectPoint(transform, width, height, u, v) {
460
+ const denominator = transform[2][0] * u + transform[2][1] * v + transform[2][2];
461
+ return {
462
+ x: width * (transform[0][0] * u + transform[0][1] * v + transform[0][2]) / denominator,
463
+ y: height * (transform[1][0] * u + transform[1][1] * v + transform[1][2]) / denominator
464
+ };
465
+ }
466
+ function sourcePoint(image, u, v) {
467
+ return { x: image.width * u, y: image.height * v };
468
+ }
469
+ function expandTriangle(points, amount) {
470
+ const center = {
471
+ x: (points[0].x + points[1].x + points[2].x) / 3,
472
+ y: (points[0].y + points[1].y + points[2].y) / 3
473
+ };
474
+ return points.map((point) => {
475
+ const length = Math.max(1, Math.hypot(point.x - center.x, point.y - center.y));
476
+ const scale = (length + amount) / length;
477
+ return { x: center.x + (point.x - center.x) * scale, y: center.y + (point.y - center.y) * scale };
478
+ });
479
+ }
480
+ function applyGraphicColor(object, color) {
481
+ if (object instanceof Group) object.getObjects().forEach((child) => applyGraphicColor(child, color));
482
+ else if (object instanceof FabricImage) {
483
+ object.filters = [new filters.BlendColor({ color, mode: "tint", alpha: 1 })];
484
+ object.applyFilters();
485
+ } else {
486
+ object.set({
487
+ fill: typeof object.fill === "string" && !isTransparentPaint(object.fill) ? color : object.fill,
488
+ stroke: typeof object.stroke === "string" && !isTransparentPaint(object.stroke) ? color : object.stroke
489
+ });
490
+ }
491
+ object.dirty = true;
492
+ }
493
+ function selectAreas(set, selector) {
494
+ if (!selector) return set.areas.map((area2, index2) => ({ area: area2, index: index2 }));
495
+ const numericIndex = /^\d+$/.test(selector) ? Number(selector) - 1 : -1;
496
+ const index = numericIndex >= 0 ? numericIndex : set.areas.findIndex((area2) => area2.id === selector || area2.name === selector);
497
+ const area = set.areas[index];
498
+ if (!area) throw new Error(`Set ${set.id} has no area matching ${selector}`);
499
+ return [{ area, index }];
500
+ }
501
+ function dataUrl(filename, contents) {
502
+ const extension = path2.extname(filename).toLowerCase();
503
+ const mime = extension === ".svg" ? "image/svg+xml" : extension === ".jpg" || extension === ".jpeg" ? "image/jpeg" : extension === ".webp" ? "image/webp" : "image/png";
504
+ return `data:${mime};base64,${contents.toString("base64")}`;
505
+ }
506
+ function safeFileNamePart(value, fallback) {
507
+ const normalized = value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "");
508
+ return normalized || fallback;
509
+ }
510
+ function isTransparentPaint(value) {
511
+ const normalized = value.trim().toLowerCase().replaceAll(" ", "");
512
+ return normalized === "none" || normalized === "transparent" || normalized === "rgba(0,0,0,0)";
513
+ }
514
+ export {
515
+ renderScreenshotSets
516
+ };
517
+ //# sourceMappingURL=node-renderer-FMVPIQAG.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/node-renderer.ts","../src/node-fonts.ts"],"sourcesContent":["import { readFile, readdir, mkdir, rm, writeFile } from \"node:fs/promises\"\nimport path from \"node:path\"\n\nimport { createCanvas, loadImage, type Canvas, type Image as CanvasImage } from \"canvas\"\nimport {\n FabricImage,\n Group,\n Rect,\n StaticCanvas,\n Textbox,\n filters,\n loadSVGFromString,\n util,\n type FabricObject,\n} from \"fabric/node\"\n\nimport { builtInArtworkById } from \"./artwork.js\"\nimport {\n MOCKUP_BUNDLE_FILENAME,\n parseMockupBundleManifest,\n resolveMockupBundle,\n type DeviceMockup,\n type Point,\n type ProjectiveTransform,\n} from \"./device-mockups.js\"\nimport { resolvePackagePublicDirectory } from \"./package-assets.js\"\nimport { registerScreenshotSetFonts } from \"./node-fonts.js\"\nimport { ProjectStore } from \"./project-store.js\"\nimport { DEFAULT_TEXT_LINE_HEIGHT_RATIO, type CanvasElement, type ScreenshotArea, type ScreenshotSet } from \"./shared.js\"\n\nexport interface RenderOptions {\n clean: boolean\n outputDirectory: string\n packageRoot: string\n scale: number\n sets: ScreenshotSet[]\n store: ProjectStore\n area?: string\n}\n\nexport interface RenderedFile {\n areaId: string\n height: number\n path: string\n setId: string\n width: number\n}\n\nexport interface RenderResult {\n files: RenderedFile[]\n outputDirectory: string\n}\n\ninterface RenderResources {\n assetSources: Map<string, string>\n mockups: Map<string, DeviceMockup>\n publicDirectory: string\n}\n\ninterface RequiredResources {\n assets: Set<string>\n mockups: Set<string>\n}\n\nexport async function renderScreenshotSets(options: RenderOptions): Promise<RenderResult> {\n if (!Number.isFinite(options.scale) || options.scale <= 0 || options.scale > 1) {\n throw new Error(\"Render scale must be greater than 0 and at most 1\")\n }\n if (options.area && options.sets.length !== 1) throw new Error(\"--area can only be used when rendering one set\")\n\n const selections = options.sets.map((set) => ({ set, areas: selectAreas(set, options.area) }))\n const selectedSets = selections.map(({ set, areas }) => ({\n ...set,\n areas: areas.map(({ area }) => area),\n }))\n await registerScreenshotSetFonts(options.packageRoot, selectedSets)\n const resources = await loadRenderResources(options.store, options.packageRoot, requiredResources(selectedSets))\n const outputDirectory = path.resolve(options.outputDirectory)\n const files: RenderedFile[] = []\n\n for (const { set, areas: selectedAreas } of selections) {\n const setDirectory = path.join(outputDirectory, set.id)\n if (options.clean) await rm(setDirectory, { recursive: true, force: true })\n await mkdir(setDirectory, { recursive: true })\n for (const { area, index } of selectedAreas) {\n const outputPath = path.join(setDirectory, `${String(index + 1).padStart(2, \"0\")}-${safeFileNamePart(area.name, `screenshot-${index + 1}`)}.png`)\n const data = await renderArea(area, set, resources, options.scale)\n await writeFile(outputPath, data)\n files.push({\n areaId: area.id,\n height: Math.round(set.canvas.height * options.scale),\n path: outputPath,\n setId: set.id,\n width: Math.round(set.canvas.width * options.scale),\n })\n }\n }\n\n return { files, outputDirectory }\n}\n\nasync function renderArea(\n area: ScreenshotArea,\n set: ScreenshotSet,\n resources: RenderResources,\n scale: number,\n): Promise<Buffer> {\n const width = Math.max(1, Math.round(set.canvas.width * scale))\n const height = Math.max(1, Math.round(set.canvas.height * scale))\n const canvas = new StaticCanvas(undefined, {\n width,\n height,\n backgroundColor: area.background,\n renderOnAddRemove: false,\n })\n\n try {\n for (const element of area.elements) {\n const object = await createFabricObject(element, resources)\n applyCanvasElement(object, element, scale)\n object.set({ evented: false, selectable: false })\n canvas.add(object)\n }\n canvas.renderAll()\n return canvas.getNodeCanvas().toBuffer(\"image/png\")\n } finally {\n canvas.dispose()\n }\n}\n\nasync function createFabricObject(element: CanvasElement, resources: RenderResources): Promise<FabricObject> {\n if (element.type === \"text\") return new Textbox(element.text, { editable: false, lockScalingFlip: true, minWidth: 24 })\n if (element.type === \"shape\") return new Rect({ lockScalingFlip: true })\n\n if (element.type === \"mockup\") {\n const screenshot = resources.assetSources.get(element.assetId)\n const mockup = resources.mockups.get(element.mockupId)\n if (!screenshot) throw new Error(`Missing asset ${element.assetId}`)\n if (!mockup) throw new Error(`Missing mockup ${element.mockupId}`)\n return renderDeviceMockup(mockup, screenshot)\n }\n\n const source = element.source.kind === \"builtin\"\n ? await builtInArtworkSource(element.source.id, resources.publicDirectory)\n : resources.assetSources.get(element.source.assetId)\n if (!source) throw new Error(`Missing image source for element ${element.id}`)\n\n let object: FabricObject\n if (source.startsWith(\"data:image/svg+xml\")) {\n const svg = Buffer.from(source.slice(source.indexOf(\",\") + 1), \"base64\").toString(\"utf8\")\n const loaded = await loadSVGFromString(svg)\n const objects = loaded.objects.filter((candidate): candidate is FabricObject => candidate !== null)\n if (objects.length === 0) throw new Error(`SVG source for element ${element.id} is empty`)\n object = util.groupSVGElements(objects, loaded.options)\n object.set({ lockScalingFlip: true })\n } else {\n object = await FabricImage.fromURL(source, {}, { imageSmoothing: true, lockScalingFlip: true })\n }\n\n if (element.fill) applyGraphicColor(object, element.fill)\n return object\n}\n\nfunction applyCanvasElement(object: FabricObject, element: CanvasElement, scale: number): void {\n object.set({\n angle: element.rotation,\n left: element.x * scale,\n opacity: element.opacity,\n originX: \"left\",\n originY: \"top\",\n top: element.y * scale,\n })\n\n if (element.type === \"text\" && object instanceof Textbox) {\n object.set({\n fill: element.color,\n fontFamily: element.fontFamily,\n fontSize: element.fontSize * scale,\n fontWeight: element.fontWeight,\n lineHeight: element.lineHeight === undefined ? DEFAULT_TEXT_LINE_HEIGHT_RATIO : element.lineHeight / element.fontSize,\n scaleX: 1,\n scaleY: 1,\n strokeWidth: 0,\n text: element.text,\n textAlign: element.textAlign,\n width: Math.max(24, element.width * scale),\n })\n object.initDimensions()\n } else if (element.type === \"shape\" && object instanceof Rect) {\n object.set({\n fill: element.fill,\n height: element.height * scale,\n rx: element.cornerRadius * scale,\n ry: element.cornerRadius * scale,\n scaleX: 1,\n scaleY: 1,\n strokeWidth: 0,\n width: element.width * scale,\n })\n } else {\n object.set({\n scaleX: (element.width * scale) / Math.max(1, object.width),\n scaleY: (element.height * scale) / Math.max(1, object.height),\n })\n }\n object.setCoords()\n}\n\nasync function loadRenderResources(store: ProjectStore, packageRoot: string, required: RequiredResources): Promise<RenderResources> {\n const [project, publicDirectory] = await Promise.all([\n store.readProject(),\n resolvePackagePublicDirectory(packageRoot),\n ])\n const assetSources = new Map<string, string>()\n await Promise.all(Object.values(project.assets).flat().filter((asset) => required.assets.has(asset.id)).map(async (asset) => {\n const filename = await store.resolveExistingAsset(asset.category, asset.name)\n assetSources.set(asset.id, dataUrl(filename, await readFile(filename)))\n }))\n\n const mockups = new Map<string, DeviceMockup>()\n await loadMockupBundle(\n path.join(publicDirectory, \"mockup-bundles/frameup-free\"),\n \"built-in\",\n mockups,\n required.mockups,\n )\n\n const entries = await readdir(store.mockupBundlesPath, { withFileTypes: true })\n for (const entry of entries) {\n if (!entry.isDirectory() || entry.name.startsWith(\".\")) continue\n if (![...required.mockups].some((mockupId) => mockupId.startsWith(`${entry.name}/`))) continue\n await loadMockupBundle(path.join(store.mockupBundlesPath, entry.name), \"project\", mockups, required.mockups, store)\n }\n\n return { assetSources, mockups, publicDirectory }\n}\n\nasync function loadMockupBundle(\n directory: string,\n origin: \"built-in\" | \"project\",\n target: Map<string, DeviceMockup>,\n requiredMockups: Set<string>,\n store?: ProjectStore,\n): Promise<void> {\n const manifest = parseMockupBundleManifest(JSON.parse(await readFile(path.join(directory, MOCKUP_BUNDLE_FILENAME), \"utf8\")))\n const catalog = resolveMockupBundle(manifest, \"\", origin)\n for (const mockup of catalog.mockups) {\n if (!requiredMockups.has(mockup.id)) continue\n const entry = manifest.mockups.find((candidate) => `${manifest.id}/${candidate.id}` === mockup.id)\n if (!entry) continue\n const framePath = store\n ? await store.resolveExistingMockupBundleFile(manifest.id, entry.frame)\n : path.join(directory, entry.frame)\n target.set(mockup.id, { ...mockup, frameUrl: dataUrl(framePath, await readFile(framePath)) })\n }\n}\n\nasync function builtInArtworkSource(id: string, publicDirectory: string): Promise<string | undefined> {\n const artwork = builtInArtworkById(id)\n if (!artwork) return undefined\n const filename = path.join(publicDirectory, artwork.url.replace(/^\\//, \"\"))\n return dataUrl(filename, await readFile(filename))\n}\n\nasync function renderDeviceMockup(mockup: DeviceMockup, screenshotUrl: string): Promise<FabricImage> {\n const [frame, screenshot] = await Promise.all([\n loadImage(mockup.frameUrl),\n loadImage(screenshotUrl),\n ])\n const canvas = createCanvas(mockup.width, mockup.height)\n const context = canvas.getContext(\"2d\")\n context.imageSmoothingEnabled = true\n\n if (mockup.screen.kind === \"rect\") {\n const screen = mockup.screen\n context.save()\n context.beginPath()\n context.roundRect(screen.x, screen.y, screen.width, screen.height, screen.cornerRadius)\n context.clip()\n context.drawImage(screenshot, screen.x, screen.y, screen.width, screen.height)\n context.restore()\n } else {\n const masked = maskRoundedScreenshot(screenshot, mockup.screen.sourceCornerRadius)\n drawProjectiveImage(context, masked, mockup.screen.transform, mockup.width, mockup.height)\n }\n\n context.drawImage(frame, 0, 0, mockup.width, mockup.height)\n return new FabricImage(canvas as unknown as HTMLCanvasElement, { imageSmoothing: true, lockScalingFlip: true })\n}\n\nfunction maskRoundedScreenshot(image: CanvasImage, radius: Point): Canvas {\n const canvas = createCanvas(image.width, image.height)\n const context = canvas.getContext(\"2d\")\n context.imageSmoothingEnabled = true\n context.beginPath()\n roundedRectPath(context, canvas.width, canvas.height, canvas.width * radius.x, canvas.height * radius.y)\n context.clip()\n context.drawImage(image, 0, 0, canvas.width, canvas.height)\n return canvas\n}\n\nfunction roundedRectPath(\n context: ReturnType<Canvas[\"getContext\"]>,\n width: number,\n height: number,\n radiusX: number,\n radiusY: number,\n): void {\n const rx = Math.min(Math.max(0, radiusX), width / 2)\n const ry = Math.min(Math.max(0, radiusY), height / 2)\n if (rx === 0 || ry === 0) {\n context.rect(0, 0, width, height)\n return\n }\n context.moveTo(rx, 0)\n context.lineTo(width - rx, 0)\n context.ellipse(width - rx, ry, rx, ry, 0, -Math.PI / 2, 0)\n context.lineTo(width, height - ry)\n context.ellipse(width - rx, height - ry, rx, ry, 0, 0, Math.PI / 2)\n context.lineTo(rx, height)\n context.ellipse(rx, height - ry, rx, ry, 0, Math.PI / 2, Math.PI)\n context.lineTo(0, ry)\n context.ellipse(rx, ry, rx, ry, 0, Math.PI, Math.PI * 1.5)\n context.closePath()\n}\n\nfunction requiredResources(sets: ScreenshotSet[]): RequiredResources {\n const assets = new Set<string>()\n const mockups = new Set<string>()\n for (const set of sets) {\n for (const area of set.areas) {\n for (const element of area.elements) {\n if (element.type === \"mockup\") {\n assets.add(element.assetId)\n mockups.add(element.mockupId)\n } else if (element.type === \"image\" && element.source.kind === \"asset\") {\n assets.add(element.source.assetId)\n }\n }\n }\n }\n return { assets, mockups }\n}\n\nfunction drawProjectiveImage(\n context: ReturnType<Canvas[\"getContext\"]>,\n image: Canvas,\n transform: ProjectiveTransform,\n frameWidth: number,\n frameHeight: number,\n): void {\n const columns = Math.max(12, Math.ceil(frameWidth / 64))\n const rows = Math.max(20, Math.ceil(frameHeight / 64))\n for (let row = 0; row < rows; row += 1) {\n for (let column = 0; column < columns; column += 1) {\n const u0 = column / columns\n const u1 = (column + 1) / columns\n const v0 = row / rows\n const v1 = (row + 1) / rows\n const destination00 = projectPoint(transform, frameWidth, frameHeight, u0, v0)\n const destination10 = projectPoint(transform, frameWidth, frameHeight, u1, v0)\n const destination11 = projectPoint(transform, frameWidth, frameHeight, u1, v1)\n const destination01 = projectPoint(transform, frameWidth, frameHeight, u0, v1)\n const source00 = sourcePoint(image, u0, v0)\n const source10 = sourcePoint(image, u1, v0)\n const source11 = sourcePoint(image, u1, v1)\n const source01 = sourcePoint(image, u0, v1)\n drawImageTriangle(context, image, [source00, source10, source11], [destination00, destination10, destination11])\n drawImageTriangle(context, image, [source00, source11, source01], [destination00, destination11, destination01])\n }\n }\n}\n\nfunction drawImageTriangle(\n context: ReturnType<Canvas[\"getContext\"]>,\n image: Canvas,\n source: [Point, Point, Point],\n destination: [Point, Point, Point],\n): void {\n const [source0, source1, source2] = source\n const [destination0, destination1, destination2] = destination\n const denominator = source0.x * (source1.y - source2.y) + source1.x * (source2.y - source0.y) + source2.x * (source0.y - source1.y)\n if (Math.abs(denominator) < Number.EPSILON) return\n const a = (destination0.x * (source1.y - source2.y) + destination1.x * (source2.y - source0.y) + destination2.x * (source0.y - source1.y)) / denominator\n const b = (destination0.y * (source1.y - source2.y) + destination1.y * (source2.y - source0.y) + destination2.y * (source0.y - source1.y)) / denominator\n const c = (destination0.x * (source2.x - source1.x) + destination1.x * (source0.x - source2.x) + destination2.x * (source1.x - source0.x)) / denominator\n const d = (destination0.y * (source2.x - source1.x) + destination1.y * (source0.x - source2.x) + destination2.y * (source1.x - source0.x)) / denominator\n const e = (destination0.x * (source1.x * source2.y - source2.x * source1.y) + destination1.x * (source2.x * source0.y - source0.x * source2.y) + destination2.x * (source0.x * source1.y - source1.x * source0.y)) / denominator\n const f = (destination0.y * (source1.x * source2.y - source2.x * source1.y) + destination1.y * (source2.x * source0.y - source0.x * source2.y) + destination2.y * (source0.x * source1.y - source1.x * source0.y)) / denominator\n context.save()\n const expanded = expandTriangle(destination, 0.75)\n context.beginPath()\n context.moveTo(expanded[0].x, expanded[0].y)\n context.lineTo(expanded[1].x, expanded[1].y)\n context.lineTo(expanded[2].x, expanded[2].y)\n context.closePath()\n context.clip()\n context.setTransform(a, b, c, d, e, f)\n context.drawImage(image, 0, 0)\n context.restore()\n}\n\nfunction projectPoint(transform: ProjectiveTransform, width: number, height: number, u: number, v: number): Point {\n const denominator = transform[2][0] * u + transform[2][1] * v + transform[2][2]\n return {\n x: width * (transform[0][0] * u + transform[0][1] * v + transform[0][2]) / denominator,\n y: height * (transform[1][0] * u + transform[1][1] * v + transform[1][2]) / denominator,\n }\n}\n\nfunction sourcePoint(image: Canvas, u: number, v: number): Point {\n return { x: image.width * u, y: image.height * v }\n}\n\nfunction expandTriangle(points: [Point, Point, Point], amount: number): [Point, Point, Point] {\n const center = {\n x: (points[0].x + points[1].x + points[2].x) / 3,\n y: (points[0].y + points[1].y + points[2].y) / 3,\n }\n return points.map((point) => {\n const length = Math.max(1, Math.hypot(point.x - center.x, point.y - center.y))\n const scale = (length + amount) / length\n return { x: center.x + (point.x - center.x) * scale, y: center.y + (point.y - center.y) * scale }\n }) as [Point, Point, Point]\n}\n\nfunction applyGraphicColor(object: FabricObject, color: string): void {\n if (object instanceof Group) object.getObjects().forEach((child) => applyGraphicColor(child, color))\n else if (object instanceof FabricImage) {\n object.filters = [new filters.BlendColor({ color, mode: \"tint\", alpha: 1 })]\n object.applyFilters()\n } else {\n object.set({\n fill: typeof object.fill === \"string\" && !isTransparentPaint(object.fill) ? color : object.fill,\n stroke: typeof object.stroke === \"string\" && !isTransparentPaint(object.stroke) ? color : object.stroke,\n })\n }\n object.dirty = true\n}\n\nfunction selectAreas(set: ScreenshotSet, selector?: string): Array<{ area: ScreenshotArea; index: number }> {\n if (!selector) return set.areas.map((area, index) => ({ area, index }))\n const numericIndex = /^\\d+$/.test(selector) ? Number(selector) - 1 : -1\n const index = numericIndex >= 0\n ? numericIndex\n : set.areas.findIndex((area) => area.id === selector || area.name === selector)\n const area = set.areas[index]\n if (!area) throw new Error(`Set ${set.id} has no area matching ${selector}`)\n return [{ area, index }]\n}\n\nfunction dataUrl(filename: string, contents: Buffer): string {\n const extension = path.extname(filename).toLowerCase()\n const mime = extension === \".svg\" ? \"image/svg+xml\"\n : extension === \".jpg\" || extension === \".jpeg\" ? \"image/jpeg\"\n : extension === \".webp\" ? \"image/webp\"\n : \"image/png\"\n return `data:${mime};base64,${contents.toString(\"base64\")}`\n}\n\nfunction safeFileNamePart(value: string, fallback: string): string {\n const normalized = value.normalize(\"NFKD\")\n .replace(/[\\u0300-\\u036f]/g, \"\")\n .replace(/[^a-zA-Z0-9._-]+/g, \"-\")\n .replace(/^-+|-+$/g, \"\")\n return normalized || fallback\n}\n\nfunction isTransparentPaint(value: string): boolean {\n const normalized = value.trim().toLowerCase().replaceAll(\" \", \"\")\n return normalized === \"none\" || normalized === \"transparent\" || normalized === \"rgba(0,0,0,0)\"\n}\n","import { createHash, randomUUID } from \"node:crypto\"\nimport { access, mkdir, rename, rm, writeFile } from \"node:fs/promises\"\nimport { tmpdir } from \"node:os\"\nimport path from \"node:path\"\n\nimport { registerFont } from \"canvas\"\n\nimport type { FontWeight, ScreenshotSet } from \"./shared.js\"\n\nconst BUNNY_FONT_ORIGIN = \"https://fonts.bunny.net\"\nconst FONT_CACHE_DIRECTORY = path.join(tmpdir(), \"storeshot-fonts-v1\")\nconst LOCAL_RENDER_FONTS = new Set([\"Geist Variable\", \"Arial\", \"Georgia\", \"Times New Roman\"])\nconst MAX_FONT_BYTES = 10 * 1024 * 1024\nconst FONT_WEIGHTS: FontWeight[] = [100, 200, 300, 400, 500, 600, 700, 800, 900]\n\ninterface RenderFontRequest {\n family: string\n text: string\n weight: FontWeight\n}\n\ninterface FontLoaderDependencies {\n cacheDirectory: string\n fetch: typeof fetch\n register: typeof registerFont\n}\n\ninterface UnicodeRange {\n end: number\n start: number\n}\n\ninterface WoffFace {\n url: string\n weight: FontWeight\n}\n\nlet bundledFontsRegistered = false\n\nexport async function registerScreenshotSetFonts(packageRoot: string, sets: ScreenshotSet[]): Promise<void> {\n registerBundledFonts(packageRoot)\n const dependencies: FontLoaderDependencies = {\n cacheDirectory: FONT_CACHE_DIRECTORY,\n fetch,\n register: registerFont,\n }\n await Promise.all(collectFontRequests(sets).map((request) => loadBunnyFont(request, dependencies)))\n}\n\nexport async function loadBunnyFont(\n request: RenderFontRequest,\n dependencies: FontLoaderDependencies,\n): Promise<void> {\n const stylesheetUrl = new URL(\"/css\", BUNNY_FONT_ORIGIN)\n stylesheetUrl.searchParams.set(\"family\", `${request.family}:${FONT_WEIGHTS.join(\",\")}`)\n stylesheetUrl.searchParams.set(\"display\", \"swap\")\n const stylesheet = await fetchText(stylesheetUrl, dependencies.fetch, `font stylesheet for ${request.family}`)\n const faces = selectWoffFaces(stylesheet, request)\n if (faces.length === 0) {\n throw new Error(`Bunny Fonts did not provide a usable font for ${request.family}`)\n }\n\n await mkdir(dependencies.cacheDirectory, { recursive: true })\n for (const { url, weight } of faces) {\n let filename = await cacheFont(url, dependencies)\n try {\n dependencies.register(filename, { family: request.family, style: \"normal\", weight: String(weight) })\n } catch {\n await rm(filename, { force: true })\n filename = await cacheFont(url, dependencies)\n dependencies.register(filename, { family: request.family, style: \"normal\", weight: String(weight) })\n }\n }\n}\n\nexport function selectWoffUrls(stylesheet: string, request: RenderFontRequest): string[] {\n return selectWoffFaces(stylesheet, request).map((face) => face.url)\n}\n\nfunction selectWoffFaces(stylesheet: string, request: RenderFontRequest): WoffFace[] {\n const candidates: WoffFace[] = []\n for (const match of stylesheet.matchAll(/@font-face\\s*\\{([\\s\\S]*?)\\}/giu)) {\n const block = match[1]\n const family = cssValue(block, \"font-family\").replace(/^(['\"])(.*)\\1$/u, \"$2\")\n const weight = Number(cssValue(block, \"font-weight\"))\n const style = cssValue(block, \"font-style\")\n if (family !== request.family || !isFontWeight(weight) || style !== \"normal\") continue\n const range = cssValue(block, \"unicode-range\")\n if (range && !textIntersectsUnicodeRange(request.text, range)) continue\n const source = cssValue(block, \"src\")\n const url = source.match(/url\\((?:['\"]?)(https:\\/\\/fonts\\.bunny\\.net\\/[^)'\"]+\\.woff(?:\\?[^)'\"]*)?)(?:['\"]?)\\)\\s*format\\((?:['\"])woff(?:['\"])\\)/iu)?.[1]\n if (url) candidates.push({ url, weight })\n }\n const availableWeights = [...new Set(candidates.map((face) => face.weight))]\n if (availableWeights.length === 0) return []\n const closestWeight = availableWeights\n .reduce((closest, weight) => Math.abs(weight - request.weight) < Math.abs(closest - request.weight) ? weight : closest, availableWeights[0])\n return candidates.filter((face) => face.weight === closestWeight)\n}\n\nfunction collectFontRequests(sets: ScreenshotSet[]): RenderFontRequest[] {\n const requests = new Map<string, RenderFontRequest>()\n for (const set of sets) {\n for (const area of set.areas) {\n for (const element of area.elements) {\n if (element.type !== \"text\" || LOCAL_RENDER_FONTS.has(element.fontFamily) || element.text.length === 0) continue\n const key = `${element.fontFamily}\\0${element.fontWeight}`\n const existing = requests.get(key)\n if (existing) existing.text += `\\n${element.text}`\n else requests.set(key, { family: element.fontFamily, text: element.text, weight: element.fontWeight })\n }\n }\n }\n return [...requests.values()]\n}\n\nfunction registerBundledFonts(packageRoot: string): void {\n if (bundledFontsRegistered) return\n try {\n registerFont(path.join(packageRoot, \"fonts/Geist-Variable.ttf\"), { family: \"Geist Variable\" })\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"ENOENT\") throw error\n }\n bundledFontsRegistered = true\n}\n\nasync function cacheFont(urlValue: string, dependencies: FontLoaderDependencies): Promise<string> {\n const url = new URL(urlValue)\n if (url.protocol !== \"https:\" || url.hostname !== \"fonts.bunny.net\") {\n throw new Error(`Refusing font URL outside ${BUNNY_FONT_ORIGIN}`)\n }\n const digest = createHash(\"sha256\").update(url.href).digest(\"hex\")\n const filename = path.join(dependencies.cacheDirectory, `${digest}.woff`)\n if (await fileExists(filename)) return filename\n\n const response = await dependencies.fetch(url)\n if (!response.ok) throw new Error(`Bunny Fonts returned ${response.status} for ${url.pathname}`)\n const declaredSize = Number(response.headers.get(\"content-length\"))\n if (Number.isFinite(declaredSize) && declaredSize > MAX_FONT_BYTES) throw new Error(`Bunny font exceeds ${MAX_FONT_BYTES} bytes`)\n const contents = Buffer.from(await response.arrayBuffer())\n if (contents.length === 0 || contents.length > MAX_FONT_BYTES) throw new Error(`Invalid Bunny font size: ${contents.length} bytes`)\n\n const temporary = `${filename}.${process.pid}.${randomUUID()}.tmp`\n await writeFile(temporary, contents, { flag: \"wx\" })\n try {\n await rename(temporary, filename)\n } catch (error) {\n if (!await fileExists(filename)) throw error\n await rm(temporary, { force: true })\n }\n return filename\n}\n\nasync function fetchText(url: URL, request: typeof fetch, description: string): Promise<string> {\n const response = await request(url, { headers: { accept: \"text/css\" } })\n if (!response.ok) throw new Error(`Could not load ${description}: Bunny Fonts returned ${response.status}`)\n return response.text()\n}\n\nfunction cssValue(block: string, property: string): string {\n const match = block.match(new RegExp(`(?:^|\\\\n)\\\\s*${property}\\\\s*:\\\\s*([^;]+)`, \"iu\"))\n return match?.[1].trim() ?? \"\"\n}\n\nfunction textIntersectsUnicodeRange(text: string, value: string): boolean {\n const ranges = value.split(\",\").flatMap(parseUnicodeRange)\n if (ranges.length === 0) return true\n return [...text].some((character) => {\n const codePoint = character.codePointAt(0)\n return codePoint !== undefined && ranges.some((range) => codePoint >= range.start && codePoint <= range.end)\n })\n}\n\nfunction parseUnicodeRange(value: string): UnicodeRange[] {\n const normalized = value.trim().replace(/^U\\+/iu, \"\")\n if (/^[0-9A-F]+-[0-9A-F]+$/iu.test(normalized)) {\n const [start, end] = normalized.split(\"-\").map((part) => Number.parseInt(part, 16))\n return [{ start, end }]\n }\n if (/^[0-9A-F?]+$/iu.test(normalized)) {\n return [{\n start: Number.parseInt(normalized.replaceAll(\"?\", \"0\"), 16),\n end: Number.parseInt(normalized.replaceAll(\"?\", \"F\"), 16),\n }]\n }\n return []\n}\n\nfunction isFontWeight(value: number): value is FontWeight {\n return Number.isInteger(value) && value >= 100 && value <= 900 && value % 100 === 0\n}\n\nasync function fileExists(filename: string): Promise<boolean> {\n try {\n await access(filename)\n return true\n } catch {\n return false\n }\n}\n"],"mappings":"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","names":["mkdir","rm","writeFile","path","path","rm","mkdir","writeFile","area","index"]}