scenri 0.6.12 → 0.6.13
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/CHANGELOG.md +18 -0
- package/dist/serve.js +279 -171
- package/package.json +1 -1
- package/studio-dist/assets/{index-BxndNQUR.css → index-B6onUYQF.css} +1 -1
- package/studio-dist/assets/index-pvL9RRJ6.js +103 -0
- package/studio-dist/index.html +2 -2
- package/studio-dist/assets/index-CloVtkXx.js +0 -103
package/dist/serve.js
CHANGED
|
@@ -10,7 +10,7 @@ import fs, { realpathSync, existsSync, readFileSync, mkdirSync, chmodSync, write
|
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import { readFile, copyFile, stat, mkdtemp, rm, readdir, writeFile } from 'fs/promises';
|
|
12
12
|
import { spawn } from 'child_process';
|
|
13
|
-
import
|
|
13
|
+
import sharp19 from 'sharp';
|
|
14
14
|
import Fastify from 'fastify';
|
|
15
15
|
import fastifyStatic from '@fastify/static';
|
|
16
16
|
import fastifyMultipart from '@fastify/multipart';
|
|
@@ -1285,7 +1285,7 @@ function createCatalogStore(db) {
|
|
|
1285
1285
|
var REFERENCE_ROLE_DIRECTIVE = {
|
|
1286
1286
|
product: "the exact product \u2014 preserve its label, shape, colors and design faithfully; do not redesign it",
|
|
1287
1287
|
character: "the exact person \u2014 match their face, facial structure, skin, hair and build exactly; their clothing, pose and background are capture context, not styling to reproduce",
|
|
1288
|
-
brand: "the brand's own mark \u2014 if the direction calls for the mark to appear, reproduce it exactly as drawn, same colours, letterforms and proportions; otherwise take only its colour and treatment, and never its subject, geometry or composition",
|
|
1288
|
+
brand: "the brand's own mark \u2014 if the direction calls for the mark to appear, reproduce it exactly as drawn, same colours, letterforms and proportions, every character down to the smallest secondary lettering, in its original script and reading direction, never translated, transliterated or re-spelled; otherwise take only its colour and treatment, and never its subject, geometry or composition",
|
|
1289
1289
|
// Only a figure-led scene attaches one of these now, so this says what that
|
|
1290
1290
|
// case actually needs. It used to read "environment and light only - take no
|
|
1291
1291
|
// subject or person from it", which handed the model a photograph of a face
|
|
@@ -1303,7 +1303,7 @@ var REFERENCE_ROLE_DIRECTIVE = {
|
|
|
1303
1303
|
var EDIT_REFERENCE_ROLE_DIRECTIVE = {
|
|
1304
1304
|
product: "the exact product: keep or restore its label, shape and design faithfully",
|
|
1305
1305
|
character: "the exact person: keep their face, facial structure, skin, hair and build faithfully; take no clothing, pose or background from this reference, and keep the source image's existing outfit unless the instruction changes it",
|
|
1306
|
-
brand: "the brand's own mark: reproduce it exactly as drawn wherever it appears, never redrawn
|
|
1306
|
+
brand: "the brand's own mark: reproduce it exactly as drawn wherever it appears \u2014 every character down to the smallest secondary lettering, in its original script and reading direction \u2014 never redrawn, re-lettered, translated or transliterated",
|
|
1307
1307
|
scene: "a reference for environment and light only",
|
|
1308
1308
|
composition: "a reference for framing and pose only",
|
|
1309
1309
|
style: "a reference for treatment and mood only",
|
|
@@ -2831,7 +2831,7 @@ function createDemoEngine(saveImage) {
|
|
|
2831
2831
|
<text x="24" y="${h - 48}" font-family="Helvetica, Arial" font-size="${Math.max(14, Math.round(w / 42))}" fill="#ffffff" opacity="0.92">${esc(label)}</text>
|
|
2832
2832
|
<text x="24" y="${h - 22}" font-family="Helvetica, Arial" font-size="12" fill="#ffffff" opacity="0.6">Scenri demo engine</text>
|
|
2833
2833
|
</svg>`;
|
|
2834
|
-
return
|
|
2834
|
+
return sharp19(Buffer.from(svg)).png().toBuffer();
|
|
2835
2835
|
}
|
|
2836
2836
|
return {
|
|
2837
2837
|
capabilities() {
|
|
@@ -3005,7 +3005,7 @@ async function resolvePresenterImages(core, templatesRoot, presenter) {
|
|
|
3005
3005
|
for (const [slot, angle] of PRESENTER_ANGLES) {
|
|
3006
3006
|
const path = presenterRefPath(templatesRoot, presenter.id, slot);
|
|
3007
3007
|
if (!existsSync(path)) continue;
|
|
3008
|
-
const png = await
|
|
3008
|
+
const png = await sharp19(readFileSync(path)).png().toBuffer();
|
|
3009
3009
|
const hash = core.images.save(png);
|
|
3010
3010
|
shots.push({ file: `asset:${hash}`, angle, locked: true });
|
|
3011
3011
|
}
|
|
@@ -3100,7 +3100,7 @@ async function resolveDemoProductImages(core, templatesRoot, product) {
|
|
|
3100
3100
|
for (const angle of angles) {
|
|
3101
3101
|
const path = demoProductRefPath(templatesRoot, product.id, angle);
|
|
3102
3102
|
if (!existsSync(path)) continue;
|
|
3103
|
-
const png = await
|
|
3103
|
+
const png = await sharp19(readFileSync(path)).png().toBuffer();
|
|
3104
3104
|
const hash = core.images.save(png);
|
|
3105
3105
|
shots.push({ file: `asset:${hash}`, angle, locked: true });
|
|
3106
3106
|
}
|
|
@@ -3216,13 +3216,92 @@ function mergeEditAttachments(own, inherited, cap2) {
|
|
|
3216
3216
|
const borrowed = inherited.filter((a) => !seen.has(a.hash)).map((a) => ({ ...a, inherited: true }));
|
|
3217
3217
|
return allocateAttachments([...own, ...borrowed], cap2);
|
|
3218
3218
|
}
|
|
3219
|
+
function joinNames(labels) {
|
|
3220
|
+
const uniq = [...new Set(labels)];
|
|
3221
|
+
if (uniq.length <= 1) return uniq[0] ?? "";
|
|
3222
|
+
return `${uniq.slice(0, -1).join(", ")} and ${uniq[uniq.length - 1]}`;
|
|
3223
|
+
}
|
|
3224
|
+
function brandContext(core, brandId) {
|
|
3225
|
+
const brand = core.store.getBrand(brandId);
|
|
3226
|
+
if (!brand) throw new Error("brand not found");
|
|
3227
|
+
const assetPaths = {};
|
|
3228
|
+
const json = brand.json;
|
|
3229
|
+
for (const logo of json.logos ?? []) {
|
|
3230
|
+
const ref = String(logo.file ?? "");
|
|
3231
|
+
if (ref.startsWith("asset:") && core.images.has(ref.slice(6))) assetPaths[ref] = core.images.pathFor(ref.slice(6));
|
|
3232
|
+
}
|
|
3233
|
+
return { brand: brand.json, assetPaths };
|
|
3234
|
+
}
|
|
3235
|
+
var PNG_SIG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
3236
|
+
var assetHash = (ref) => {
|
|
3237
|
+
const s = String(ref ?? "");
|
|
3238
|
+
return s.startsWith("asset:") ? s.slice(6) : null;
|
|
3239
|
+
};
|
|
3240
|
+
var LOGO_ROLES = ["primary", "mark", "wordmark", "monochrome", "alternate"];
|
|
3241
|
+
var LOGO_BACKGROUNDS = ["light", "dark", "any"];
|
|
3242
|
+
var toPng = (buf) => sharp19(buf).rotate().png().toBuffer();
|
|
3243
|
+
var COST_PROBE = {
|
|
3244
|
+
prompt: "",
|
|
3245
|
+
brand: { brand: {}, assetPaths: {} },
|
|
3246
|
+
width: 1024,
|
|
3247
|
+
height: 1024,
|
|
3248
|
+
count: 1
|
|
3249
|
+
};
|
|
3250
|
+
var MARK_MAX_EDGE = 2048;
|
|
3251
|
+
var MARK_MIN_EDGE = 1024;
|
|
3252
|
+
var MARK_TINY_EDGE = 256;
|
|
3253
|
+
var MARK_WARN_EDGE = 512;
|
|
3254
|
+
var toMarkPng = async (buf) => {
|
|
3255
|
+
const out = await sharp19(buf, { density: 384 }).rotate().resize({ width: MARK_MAX_EDGE, height: MARK_MAX_EDGE, fit: "inside", withoutEnlargement: true }).png().toBuffer();
|
|
3256
|
+
const meta = await sharp19(out).metadata();
|
|
3257
|
+
const edge = Math.max(meta.width ?? 0, meta.height ?? 0);
|
|
3258
|
+
if (edge >= MARK_TINY_EDGE && edge < MARK_MIN_EDGE) {
|
|
3259
|
+
return sharp19(out).resize({ width: MARK_MIN_EDGE, height: MARK_MIN_EDGE, fit: "inside", kernel: "lanczos3" }).png().toBuffer();
|
|
3260
|
+
}
|
|
3261
|
+
return out;
|
|
3262
|
+
};
|
|
3263
|
+
var cappedRefs = /* @__PURE__ */ new Map();
|
|
3264
|
+
async function capReferenceEdge(core, path, maxEdge) {
|
|
3265
|
+
const key = `${path}#${maxEdge}`;
|
|
3266
|
+
const hit = cappedRefs.get(key);
|
|
3267
|
+
if (hit) return hit;
|
|
3268
|
+
let out = path;
|
|
3269
|
+
try {
|
|
3270
|
+
const meta = await sharp19(path).metadata();
|
|
3271
|
+
if ((meta.width ?? 0) > maxEdge || (meta.height ?? 0) > maxEdge) {
|
|
3272
|
+
const buf = await sharp19(path).resize({ width: maxEdge, height: maxEdge, fit: "inside", withoutEnlargement: true }).png().toBuffer();
|
|
3273
|
+
out = core.images.pathFor(core.images.save(buf));
|
|
3274
|
+
}
|
|
3275
|
+
} catch {
|
|
3276
|
+
}
|
|
3277
|
+
cappedRefs.set(key, out);
|
|
3278
|
+
return out;
|
|
3279
|
+
}
|
|
3280
|
+
var readImagePart = async (core, req, normalize2) => {
|
|
3281
|
+
const part = await req.file();
|
|
3282
|
+
if (!part) return { error: "multipart file field required" };
|
|
3283
|
+
const buf = await part.toBuffer();
|
|
3284
|
+
if (buf.length === 0) return { error: "empty file" };
|
|
3285
|
+
try {
|
|
3286
|
+
return { hash: core.images.save(await normalize2(buf)), fields: part.fields ?? {}, filename: part.filename };
|
|
3287
|
+
} catch {
|
|
3288
|
+
return { error: "that file is not an image we can read" };
|
|
3289
|
+
}
|
|
3290
|
+
};
|
|
3291
|
+
var mtimeQS = (path) => existsSync(path) ? `?v=${Math.round(statSync(path).mtimeMs)}` : "";
|
|
3292
|
+
var serveJpeg = (req, reply, path) => {
|
|
3293
|
+
const etag = `"${statSync(path).mtimeMs}"`;
|
|
3294
|
+
reply.header("cache-control", "public, max-age=31536000, immutable").header("etag", etag);
|
|
3295
|
+
if (req.headers["if-none-match"] === etag) return reply.status(304).send();
|
|
3296
|
+
return reply.header("content-type", "image/jpeg").send(readFileSync(path));
|
|
3297
|
+
};
|
|
3219
3298
|
|
|
3220
3299
|
// src/briefDirectives.ts
|
|
3221
3300
|
function productFidelityDirective(attached) {
|
|
3222
3301
|
if (attached <= 1) {
|
|
3223
3302
|
return "The attached product image is the exact product: preserve its label, shape, colors and proportions faithfully, and do not redesign it. It is also the only view of this product that exists. Any face, side or detail not visible in it is unknown \u2014 keep those plain and consistent with the visible materials and color, and do not invent hardware, text, seams, closures, ornament or branding on them. Prefer a composition that shows the product from the view the reference gives.";
|
|
3224
3303
|
}
|
|
3225
|
-
return "The attached product images all show the exact
|
|
3304
|
+
return "The attached product images all show the exact product to feature: preserve its label, shape and proportions faithfully, do not redesign it, and never treat an extra image as an additional product. The first product image is the authority for its color, finish and material. Where another image differs in color or finish, it shows the same product in another colorway \u2014 never blend colorways, and render the one the first image shows. Any face not visible in them is unknown \u2014 keep it plain and consistent with the materials the first image shows, and do not invent detail on it. If the direction above explicitly asks for more than one colorway, that explicit request wins.";
|
|
3226
3305
|
}
|
|
3227
3306
|
function editPreservationDirective(scope, opts) {
|
|
3228
3307
|
if (scope === "local") {
|
|
@@ -3241,6 +3320,10 @@ function productFactDirectives(p) {
|
|
|
3241
3320
|
const materials = p.materials ?? p.material;
|
|
3242
3321
|
if (materials) out.push(`Its materials and finish: ${materials}.`);
|
|
3243
3322
|
if (p.primaryColors) out.push(`Its actual colors: ${p.primaryColors}.`);
|
|
3323
|
+
if (Array.isArray(p.colorways) && p.colorways.length)
|
|
3324
|
+
out.push(
|
|
3325
|
+
`It is sold in these colorways: ${p.colorways.join(", ")}. The one in this shot is the colorway the first product image shows.`
|
|
3326
|
+
);
|
|
3244
3327
|
if (p.dimensions)
|
|
3245
3328
|
out.push(`Its real-world size is ${p.dimensions} \u2014 keep it at true scale relative to everything else in frame.`);
|
|
3246
3329
|
return out;
|
|
@@ -3258,7 +3341,7 @@ function characterEditIdentityDirective(name) {
|
|
|
3258
3341
|
return `${name} is the person in this photograph: keep them present and clearly visible. Match their face, facial structure, skin, hair and build to the attached person reference exactly. The reference's plain outfit and studio backdrop are capture conditions, not direction: keep the styling this photograph already has unless the instruction itself changes it, and never return them to the plain base layers they were photographed in.`;
|
|
3259
3342
|
}
|
|
3260
3343
|
function markEditDirective() {
|
|
3261
|
-
return "The attached brand mark is this brand's own mark: wherever the logo appears or the instruction asks for it, reproduce it exactly as drawn \u2014 same colours, letterforms and proportions \u2014 never redrawn or re-lettered.";
|
|
3344
|
+
return "The attached brand mark is this brand's own mark: wherever the logo appears or the instruction asks for it, reproduce it exactly as drawn \u2014 same colours, letterforms and proportions \u2014 never redrawn or re-lettered. Every character it carries stays intact, including the smallest secondary lettering, in its original script and reading direction \u2014 never translated, transliterated or re-spelled.";
|
|
3262
3345
|
}
|
|
3263
3346
|
function personSkinDirective() {
|
|
3264
3347
|
return "Every person in this photograph has real photographed skin: fine natural texture at pore scale, faint lines and natural asymmetry left intact, true-to-life proportions, never airbrushed, waxy, plastic or synthetic-looking. Light behaves physically on it: a hard flash or a specular key may genuinely shine on skin, but the surface underneath stays living skin in a photograph, never gloss, never a render. Any retouch, clarity or sharpness language in the direction above is about finish and focus, never a licence to smooth skin beyond what a professional photograph holds; if the direction above explicitly asks for a stylised or non-photographic treatment of the person, that explicit request wins.";
|
|
@@ -3296,7 +3379,7 @@ function sceneFigureDirectives(opts) {
|
|
|
3296
3379
|
// instruction to mutate the one real mark the user deliberately
|
|
3297
3380
|
// attached. Same shape as pairDirectives' packshot override - name the
|
|
3298
3381
|
// scope of the earlier rule, carry the exception in the same breath.
|
|
3299
|
-
(opts.hasMark ? " The one exception is the attached brand mark: it is this brand's own real mark, deliberately attached, so the fictional-brands rule above does not apply to it. Where the direction asks for that mark to appear, it appears exactly as drawn - never redrawn, re-lettered or fictionalised." : "")
|
|
3382
|
+
(opts.hasMark ? " The one exception is the attached brand mark: it is this brand's own real mark, deliberately attached, so the fictional-brands rule above does not apply to it. Where the direction asks for that mark to appear, it appears exactly as drawn - every character, including the smallest lettering, in its original script - never redrawn, re-lettered, translated or fictionalised." : "")
|
|
3300
3383
|
);
|
|
3301
3384
|
}
|
|
3302
3385
|
return out;
|
|
@@ -3365,7 +3448,7 @@ var FORMATS = [
|
|
|
3365
3448
|
{ id: "landscape", label: "Landscape 16:9", w: 1600, h: 900 },
|
|
3366
3449
|
{ id: "portrait", label: "Portrait 4:5", w: 1024, h: 1280 }
|
|
3367
3450
|
];
|
|
3368
|
-
var
|
|
3451
|
+
var assetHash2 = (ref) => {
|
|
3369
3452
|
const s = String(ref ?? "");
|
|
3370
3453
|
return s.startsWith("asset:") ? s.slice(6) : null;
|
|
3371
3454
|
};
|
|
@@ -3428,6 +3511,9 @@ function compileBrief(brief, ctx) {
|
|
|
3428
3511
|
const append = (s) => {
|
|
3429
3512
|
sentence += (sentence && !sentence.endsWith(" ") ? " " : "") + s;
|
|
3430
3513
|
};
|
|
3514
|
+
const attachingMarkHashes = new Set(
|
|
3515
|
+
brief.tokens.filter((t) => t?.t === "mark").map((t) => t.imageHash).filter((h) => (ctx.brand?.logos ?? []).some((l) => assetHash2(l?.file) === h) && ctx.images.has(h))
|
|
3516
|
+
);
|
|
3431
3517
|
for (const tok of brief.tokens) {
|
|
3432
3518
|
switch (tok.t) {
|
|
3433
3519
|
case "text":
|
|
@@ -3446,7 +3532,7 @@ function compileBrief(brief, ctx) {
|
|
|
3446
3532
|
const phashes = [];
|
|
3447
3533
|
for (const s of orderedShots) {
|
|
3448
3534
|
if (phashes.length >= PRODUCT_REF_MAX) break;
|
|
3449
|
-
const h =
|
|
3535
|
+
const h = assetHash2(s?.file);
|
|
3450
3536
|
if (h && ctx.images.has(h) && !phashes.includes(h)) phashes.push(h);
|
|
3451
3537
|
}
|
|
3452
3538
|
if (phashes.length) {
|
|
@@ -3472,7 +3558,7 @@ function compileBrief(brief, ctx) {
|
|
|
3472
3558
|
}
|
|
3473
3559
|
hasPerson = true;
|
|
3474
3560
|
append(c.promptName ?? c.name);
|
|
3475
|
-
const chashes = (c.shots ?? []).slice(0, CHARACTER_REF_MAX).map((s) =>
|
|
3561
|
+
const chashes = (c.shots ?? []).slice(0, CHARACTER_REF_MAX).map((s) => assetHash2(s?.file)).filter((h) => !!h && ctx.images.has(h));
|
|
3476
3562
|
if (chashes.length) {
|
|
3477
3563
|
chashes.forEach((chash, i) => {
|
|
3478
3564
|
attachments.push({ role: "character", id: c.id, label: c.name, hash: chash, essential: i === 0 });
|
|
@@ -3500,6 +3586,10 @@ function compileBrief(brief, ctx) {
|
|
|
3500
3586
|
break;
|
|
3501
3587
|
}
|
|
3502
3588
|
case "ref": {
|
|
3589
|
+
if (attachingMarkHashes.has(tok.imageHash)) {
|
|
3590
|
+
warnings.push("That reference is the same image as your brand mark, so it rides once, as the mark.");
|
|
3591
|
+
break;
|
|
3592
|
+
}
|
|
3503
3593
|
if (!ctx.images.has(tok.imageHash)) {
|
|
3504
3594
|
warnings.push("A reference shot is missing and was skipped.");
|
|
3505
3595
|
break;
|
|
@@ -3510,14 +3600,23 @@ function compileBrief(brief, ctx) {
|
|
|
3510
3600
|
}
|
|
3511
3601
|
case "mark": {
|
|
3512
3602
|
const logos = ctx.brand?.logos ?? [];
|
|
3513
|
-
const logo = logos.find((l) =>
|
|
3603
|
+
const logo = logos.find((l) => assetHash2(l?.file) === tok.imageHash);
|
|
3514
3604
|
if (!logo || !ctx.images.has(tok.imageHash)) {
|
|
3515
3605
|
warnings.push("A brand mark in this brief is no longer in the kit.");
|
|
3516
3606
|
break;
|
|
3517
3607
|
}
|
|
3518
3608
|
attachments.push({ role: "brand", label: markLabel(ctx.brand, logo), hash: tok.imageHash });
|
|
3609
|
+
try {
|
|
3610
|
+
const head = ctx.images.read(tok.imageHash);
|
|
3611
|
+
const edge = Math.max(head.readUInt32BE(16), head.readUInt32BE(20));
|
|
3612
|
+
if (edge < MARK_WARN_EDGE)
|
|
3613
|
+
warnings.push(
|
|
3614
|
+
`${markLabel(ctx.brand, logo)} is only ${edge}px across, so fine lettering will not survive generation. Export it larger, or as SVG.`
|
|
3615
|
+
);
|
|
3616
|
+
} catch {
|
|
3617
|
+
}
|
|
3519
3618
|
otherDirectives.push(
|
|
3520
|
-
"The attached brand mark is this brand's own mark. If the direction asks for the logo to appear, reproduce it exactly as drawn \u2014 same colours, letterforms and proportions, never redrawn or re-lettered. Otherwise take only its colour and treatment from it."
|
|
3619
|
+
"The attached brand mark is this brand's own mark. If the direction asks for the logo to appear, reproduce it exactly as drawn \u2014 same colours, letterforms and proportions, never redrawn or re-lettered. Every character it carries appears intact, including the smallest secondary lettering, in its original script and reading direction \u2014 never translated, transliterated or re-spelled. Otherwise take only its colour and treatment from it."
|
|
3521
3620
|
);
|
|
3522
3621
|
break;
|
|
3523
3622
|
}
|
|
@@ -3535,7 +3634,7 @@ function compileBrief(brief, ctx) {
|
|
|
3535
3634
|
append(composePrompt(t, { fields: brief.templateFields ?? {}, notes: "" }));
|
|
3536
3635
|
if (ctx.mode !== "edit" && t.figure) {
|
|
3537
3636
|
for (const r of (t.refs ?? []).slice(0, SCENE_REF_MAX)) {
|
|
3538
|
-
const h =
|
|
3637
|
+
const h = assetHash2(r?.file);
|
|
3539
3638
|
if (h && ctx.images.has(h)) {
|
|
3540
3639
|
attachments.push({ role: "scene", id: t.id, label: t.name, hash: h, essential: false });
|
|
3541
3640
|
}
|
|
@@ -4334,6 +4433,7 @@ async function buildFromUrl(url, opts = {}) {
|
|
|
4334
4433
|
if (!palette.primary) warnings.push("No confident palette found. Set colors manually.");
|
|
4335
4434
|
let logoRef;
|
|
4336
4435
|
let logoFromOg = false;
|
|
4436
|
+
let logoTiny = false;
|
|
4337
4437
|
let iconHref = $('link[rel="apple-touch-icon"]').attr("href") || $('link[rel~="icon"]').attr("href");
|
|
4338
4438
|
if (!iconHref) {
|
|
4339
4439
|
iconHref = $('meta[property="og:image"]').attr("content");
|
|
@@ -4344,7 +4444,18 @@ async function buildFromUrl(url, opts = {}) {
|
|
|
4344
4444
|
const iconRes = await fetchImpl(new URL(iconHref, origin).toString(), { redirect: "follow" });
|
|
4345
4445
|
if (iconRes.ok) {
|
|
4346
4446
|
const buf = Buffer.from(await iconRes.arrayBuffer());
|
|
4347
|
-
if (buf.length > 0)
|
|
4447
|
+
if (buf.length > 0) {
|
|
4448
|
+
logoRef = await opts.saveAsset(buf, "logo");
|
|
4449
|
+
if (!logoFromOg && opts.probeLongEdge) {
|
|
4450
|
+
const edge = await opts.probeLongEdge(buf).catch(() => null);
|
|
4451
|
+
if (edge !== null && edge < 256) {
|
|
4452
|
+
logoTiny = true;
|
|
4453
|
+
warnings.push(
|
|
4454
|
+
`The site icon is favicon-sized (${edge}px), so it was saved as an alternate mark, not the logo. Upload your real logo in Settings.`
|
|
4455
|
+
);
|
|
4456
|
+
}
|
|
4457
|
+
}
|
|
4458
|
+
}
|
|
4348
4459
|
}
|
|
4349
4460
|
} catch {
|
|
4350
4461
|
warnings.push("Logo download failed.");
|
|
@@ -4373,7 +4484,7 @@ async function buildFromUrl(url, opts = {}) {
|
|
|
4373
4484
|
...palette.neutrals.length ? { neutrals: palette.neutrals.map((hex) => ({ hex })) } : {}
|
|
4374
4485
|
}
|
|
4375
4486
|
} : {},
|
|
4376
|
-
...logoRef ? { logos: [{ role: logoFromOg ? "alternate" : "primary", file: logoRef }] } : {}
|
|
4487
|
+
...logoRef ? { logos: [{ role: logoFromOg || logoTiny ? "alternate" : "primary", file: logoRef }] } : {}
|
|
4377
4488
|
};
|
|
4378
4489
|
return { brand, warnings };
|
|
4379
4490
|
}
|
|
@@ -5645,8 +5756,8 @@ async function runJob(deps, jobId, brandId, url, signal) {
|
|
|
5645
5756
|
errors.push({ code: "image_empty", message: "Empty image", url: img.sourceUrl });
|
|
5646
5757
|
return;
|
|
5647
5758
|
}
|
|
5648
|
-
const png = await
|
|
5649
|
-
const meta = await
|
|
5759
|
+
const png = await sharp19(buf).rotate().png().toBuffer();
|
|
5760
|
+
const meta = await sharp19(png).metadata();
|
|
5650
5761
|
const hash = core.images.save(png);
|
|
5651
5762
|
core.catalog.setImageAsset(img.productId, img.sourceUrl, `asset:${hash}`, {
|
|
5652
5763
|
width: meta.width,
|
|
@@ -5703,24 +5814,54 @@ function resolveLibraryProduct(core, brandId, productId) {
|
|
|
5703
5814
|
const library = core.catalog.listLibraryProducts(brandId, brand.json);
|
|
5704
5815
|
return library.find((p) => p.id === productId) ?? null;
|
|
5705
5816
|
}
|
|
5817
|
+
function stripHtml(html) {
|
|
5818
|
+
return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/ /g, " ").replace(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/g, ">").replace(/\s+/g, " ").trim();
|
|
5819
|
+
}
|
|
5820
|
+
function firstSentence(text, max) {
|
|
5821
|
+
if (text.length <= max) return text;
|
|
5822
|
+
const stop = text.slice(0, max + 1).search(/[.!?](\s|$)/);
|
|
5823
|
+
if (stop > 40) return text.slice(0, stop + 1);
|
|
5824
|
+
const cut = text.lastIndexOf(" ", max);
|
|
5825
|
+
return `${text.slice(0, cut > 40 ? cut : max).trimEnd()}\u2026`;
|
|
5826
|
+
}
|
|
5827
|
+
function colorwaysOf(variants) {
|
|
5828
|
+
const seen = [];
|
|
5829
|
+
for (const v of variants ?? []) {
|
|
5830
|
+
const key = Object.keys(v.options ?? {}).find((k) => /colou?r/i.test(k));
|
|
5831
|
+
const val = key ? String(v.options[key]).trim() : "";
|
|
5832
|
+
if (val && !seen.includes(val)) seen.push(val);
|
|
5833
|
+
if (seen.length >= 12) break;
|
|
5834
|
+
}
|
|
5835
|
+
return seen.length >= 2 ? seen : [];
|
|
5836
|
+
}
|
|
5706
5837
|
function brandJsonWithCatalogProducts(core, brandId) {
|
|
5707
5838
|
const brand = core.store.getBrand(brandId);
|
|
5708
5839
|
if (!brand) return null;
|
|
5709
5840
|
const json = { ...brand.json };
|
|
5710
5841
|
const library = core.catalog.listLibraryProducts(brandId, brand.json);
|
|
5711
|
-
json.products = library.map((p) =>
|
|
5712
|
-
|
|
5713
|
-
|
|
5714
|
-
|
|
5715
|
-
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
|
|
5720
|
-
|
|
5721
|
-
|
|
5722
|
-
|
|
5723
|
-
|
|
5842
|
+
json.products = library.map((p) => {
|
|
5843
|
+
const colorways = colorwaysOf(p.variants);
|
|
5844
|
+
return {
|
|
5845
|
+
id: p.id,
|
|
5846
|
+
name: p.name,
|
|
5847
|
+
shots: p.shots,
|
|
5848
|
+
notes: p.url ?? void 0,
|
|
5849
|
+
// Forwarded so the compiler can state real-world material and size. These
|
|
5850
|
+
// were dropped here, which is part of why a watch could render plate-sized:
|
|
5851
|
+
// nothing downstream ever knew how big the object actually is.
|
|
5852
|
+
...p.category ? { category: p.category } : {},
|
|
5853
|
+
...p.variant ? { variant: p.variant } : {},
|
|
5854
|
+
...p.material ? { material: p.material } : {},
|
|
5855
|
+
...p.dimensions ? { dimensions: p.dimensions } : {},
|
|
5856
|
+
// The store's own words, the way the 0.6.9 fix gave demo products
|
|
5857
|
+
// theirs: the description is what anchors scale when dimensions are
|
|
5858
|
+
// absent, and imported products shipped with neither.
|
|
5859
|
+
...p.descriptionHtml ? { description: firstSentence(stripHtml(String(p.descriptionHtml)), 300) } : {},
|
|
5860
|
+
// The declared colorways, so the compiler can say a colour difference
|
|
5861
|
+
// between references is a colorway rather than lighting.
|
|
5862
|
+
...colorways.length ? { colorways } : {}
|
|
5863
|
+
};
|
|
5864
|
+
});
|
|
5724
5865
|
return json;
|
|
5725
5866
|
}
|
|
5726
5867
|
function runningImportCount() {
|
|
@@ -6065,7 +6206,7 @@ async function generateStudioSet(deps, job, who, sourcePaths, signal) {
|
|
|
6065
6206
|
return STUDIO_FRAMES.map((f) => byAngle.get(f.angle)).filter((h) => !!h);
|
|
6066
6207
|
}
|
|
6067
6208
|
async function edgeBarGeometry(buf) {
|
|
6068
|
-
const { data, info } = await
|
|
6209
|
+
const { data, info } = await sharp19(buf).greyscale().raw().toBuffer({ resolveWithObject: true });
|
|
6069
6210
|
const W = info.width;
|
|
6070
6211
|
const H = info.height;
|
|
6071
6212
|
const scan = (len, cross, at) => {
|
|
@@ -6119,7 +6260,7 @@ async function trimEdgeBars(core, hash) {
|
|
|
6119
6260
|
const width = g.right - g.left + 1;
|
|
6120
6261
|
const height = g.bottom - g.top + 1;
|
|
6121
6262
|
if (width < g.W * 0.6 || height < g.H * 0.6) return hash;
|
|
6122
|
-
const png = await
|
|
6263
|
+
const png = await sharp19(buf).extract({ left: g.left, top: g.top, width, height }).png().toBuffer();
|
|
6123
6264
|
return core.images.save(png);
|
|
6124
6265
|
} catch {
|
|
6125
6266
|
return hash;
|
|
@@ -6161,12 +6302,12 @@ async function avatarCrop(core, hash) {
|
|
|
6161
6302
|
);
|
|
6162
6303
|
}
|
|
6163
6304
|
async function figureBox(buf) {
|
|
6164
|
-
const meta = await
|
|
6305
|
+
const meta = await sharp19(buf).metadata();
|
|
6165
6306
|
const W = meta.width ?? 0;
|
|
6166
6307
|
const H = meta.height ?? 0;
|
|
6167
6308
|
if (!W || !H) return null;
|
|
6168
6309
|
for (const threshold of FIGURE_TRIM_THRESHOLDS) {
|
|
6169
|
-
const { info } = await
|
|
6310
|
+
const { info } = await sharp19(buf).trim({ threshold }).toBuffer({ resolveWithObject: true });
|
|
6170
6311
|
const left = Math.abs(info.trimOffsetLeft ?? 0);
|
|
6171
6312
|
const top = Math.abs(info.trimOffsetTop ?? 0);
|
|
6172
6313
|
const width = info.width ?? 0;
|
|
@@ -6199,13 +6340,13 @@ async function smartCover(core, hash, box) {
|
|
|
6199
6340
|
if (!hash || !core.images.has(hash)) return void 0;
|
|
6200
6341
|
try {
|
|
6201
6342
|
const buf = core.images.read(hash);
|
|
6202
|
-
const meta = await
|
|
6343
|
+
const meta = await sharp19(buf).metadata();
|
|
6203
6344
|
const w = meta.width ?? 0;
|
|
6204
6345
|
const h = meta.height ?? 0;
|
|
6205
6346
|
if (!w || !h) return void 0;
|
|
6206
6347
|
const raw = box(w, h);
|
|
6207
6348
|
const target = { width: Math.max(1, raw.width), height: Math.max(1, raw.height) };
|
|
6208
|
-
const png = await
|
|
6349
|
+
const png = await sharp19(buf).resize(target.width, target.height, { fit: "cover", position: "attention" }).png().toBuffer();
|
|
6209
6350
|
return core.images.save(png);
|
|
6210
6351
|
} catch {
|
|
6211
6352
|
return void 0;
|
|
@@ -6214,11 +6355,11 @@ async function smartCover(core, hash, box) {
|
|
|
6214
6355
|
async function crop(core, hash, region, cap2) {
|
|
6215
6356
|
if (!hash || !core.images.has(hash)) return void 0;
|
|
6216
6357
|
try {
|
|
6217
|
-
const meta = await
|
|
6358
|
+
const meta = await sharp19(core.images.read(hash)).metadata();
|
|
6218
6359
|
const w = meta.width ?? 0;
|
|
6219
6360
|
const h = meta.height ?? 0;
|
|
6220
6361
|
if (!w || !h) return void 0;
|
|
6221
|
-
let pipeline =
|
|
6362
|
+
let pipeline = sharp19(core.images.read(hash)).extract(region(w, h));
|
|
6222
6363
|
if (cap2) pipeline = pipeline.resize(cap2, cap2, { fit: "inside", withoutEnlargement: true });
|
|
6223
6364
|
const png = await pipeline.png().toBuffer();
|
|
6224
6365
|
return core.images.save(png);
|
|
@@ -6402,6 +6543,7 @@ function registerAccessGuard(app, opts = {}) {
|
|
|
6402
6543
|
var MAX_HOPS = 64;
|
|
6403
6544
|
var IDENTITY_KINDS = /* @__PURE__ */ new Set(["product", "character", "mark", "ref"]);
|
|
6404
6545
|
var identityOf = (list2) => Array.isArray(list2) ? list2.filter((t) => IDENTITY_KINDS.has(t?.t)) : [];
|
|
6546
|
+
var identityTokenKey = (t) => t?.t === "product" ? `p:${t.id}` : JSON.stringify(t);
|
|
6405
6547
|
function inheritedIdentityTokens(parentId, getNode) {
|
|
6406
6548
|
let id = parentId;
|
|
6407
6549
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -6417,7 +6559,7 @@ function inheritedIdentityTokens(parentId, getNode) {
|
|
|
6417
6559
|
const seen = /* @__PURE__ */ new Set();
|
|
6418
6560
|
const tokens = [];
|
|
6419
6561
|
for (const t of [...own, ...carried]) {
|
|
6420
|
-
const key =
|
|
6562
|
+
const key = identityTokenKey(t);
|
|
6421
6563
|
if (seen.has(key)) continue;
|
|
6422
6564
|
seen.add(key);
|
|
6423
6565
|
tokens.push(t);
|
|
@@ -6570,7 +6712,7 @@ function planCrop(source, targetRatio) {
|
|
|
6570
6712
|
}
|
|
6571
6713
|
async function attentionCropOrigin(srcBuf, source, plan) {
|
|
6572
6714
|
try {
|
|
6573
|
-
const { info } = await
|
|
6715
|
+
const { info } = await sharp19(srcBuf).resize(plan.width, plan.height, { fit: "cover", position: "attention" }).toBuffer({ resolveWithObject: true });
|
|
6574
6716
|
const attnLeft = typeof info.cropOffsetLeft === "number" ? Math.abs(info.cropOffsetLeft) : plan.left;
|
|
6575
6717
|
const attnTop = typeof info.cropOffsetTop === "number" ? Math.abs(info.cropOffsetTop) : plan.top;
|
|
6576
6718
|
const left = Math.round((attnLeft + plan.left) / 2);
|
|
@@ -6723,23 +6865,23 @@ function relax(grid, seam, fixedSweeps) {
|
|
|
6723
6865
|
|
|
6724
6866
|
// src/expand.ts
|
|
6725
6867
|
async function expandCanvas(source, plan) {
|
|
6726
|
-
const bed = await
|
|
6727
|
-
return
|
|
6868
|
+
const bed = await sharp19(source).resize(plan.width, plan.height, { fit: "cover", position: "centre" }).blur(Math.max(8, Math.round(Math.max(plan.width, plan.height) / 40))).toBuffer();
|
|
6869
|
+
return sharp19(bed).composite([{ input: source, left: plan.left, top: plan.top }]).png().toBuffer();
|
|
6728
6870
|
}
|
|
6729
6871
|
async function compositeExpand(engineImage, source, plan) {
|
|
6730
|
-
const meta = await
|
|
6872
|
+
const meta = await sharp19(engineImage).metadata();
|
|
6731
6873
|
const want = plan.width / plan.height;
|
|
6732
6874
|
const got = meta.width && meta.height ? meta.width / meta.height : 0;
|
|
6733
6875
|
const sameOrientation = got > 0 && got >= 1 === want >= 1;
|
|
6734
6876
|
const aligned = sameOrientation;
|
|
6735
6877
|
const exact = meta.width === plan.width && meta.height === plan.height;
|
|
6736
|
-
const surround = aligned ? exact ? engineImage : await
|
|
6878
|
+
const surround = aligned ? exact ? engineImage : await sharp19(engineImage).resize(plan.width, plan.height, { fit: "cover", position: "centre" }).toBuffer() : await expandCanvasBedOnly(source, plan);
|
|
6737
6879
|
const matched = aligned ? await matchMarginsToSeam(surround, source, plan) : surround;
|
|
6738
|
-
const image = await
|
|
6880
|
+
const image = await sharp19(matched).composite([{ input: source, left: plan.left, top: plan.top }]).png().toBuffer();
|
|
6739
6881
|
return { image, aligned };
|
|
6740
6882
|
}
|
|
6741
6883
|
async function matchMarginsToSeam(surround, source, plan) {
|
|
6742
|
-
const src = await
|
|
6884
|
+
const src = await sharp19(source).metadata();
|
|
6743
6885
|
if (!src.width || !src.height) return surround;
|
|
6744
6886
|
const SW = src.width;
|
|
6745
6887
|
const SH = src.height;
|
|
@@ -6786,8 +6928,8 @@ var MAX_CORRECTION = 60;
|
|
|
6786
6928
|
async function reconcile(surround, source, side, axis) {
|
|
6787
6929
|
const { margin } = side;
|
|
6788
6930
|
if (margin.width < 1 || margin.height < 1) return surround;
|
|
6789
|
-
const marginRaw = await
|
|
6790
|
-
const edgeRaw = await
|
|
6931
|
+
const marginRaw = await sharp19(surround).extract(margin).removeAlpha().raw().toBuffer();
|
|
6932
|
+
const edgeRaw = await sharp19(source).extract(side.srcEdge).removeAlpha().raw().toBuffer();
|
|
6791
6933
|
const W = margin.width;
|
|
6792
6934
|
const H = margin.height;
|
|
6793
6935
|
const along = axis === "width" ? H : W;
|
|
@@ -6841,11 +6983,11 @@ async function reconcile(surround, source, side, axis) {
|
|
|
6841
6983
|
}
|
|
6842
6984
|
}
|
|
6843
6985
|
}
|
|
6844
|
-
const patch2 = await
|
|
6845
|
-
return
|
|
6986
|
+
const patch2 = await sharp19(corrected, { raw: { width: W, height: H, channels: 3 } }).png().toBuffer();
|
|
6987
|
+
return sharp19(surround).composite([{ input: patch2, left: margin.left, top: margin.top }]).toBuffer();
|
|
6846
6988
|
}
|
|
6847
6989
|
async function expandCanvasBedOnly(source, plan) {
|
|
6848
|
-
return
|
|
6990
|
+
return sharp19(source).resize(plan.width, plan.height, { fit: "cover", position: "centre" }).blur(Math.max(8, Math.round(Math.max(plan.width, plan.height) / 40))).toBuffer();
|
|
6849
6991
|
}
|
|
6850
6992
|
function medianOf(rgb, channel, from, to) {
|
|
6851
6993
|
const n = to - from;
|
|
@@ -6856,17 +6998,17 @@ function medianOf(rgb, channel, from, to) {
|
|
|
6856
6998
|
return n % 2 ? values[(n - 1) / 2] : (values[n / 2 - 1] + values[n / 2]) / 2;
|
|
6857
6999
|
}
|
|
6858
7000
|
async function reframeExpand(engineImage, plan) {
|
|
6859
|
-
const meta = await
|
|
7001
|
+
const meta = await sharp19(engineImage).metadata();
|
|
6860
7002
|
if (!(meta.width && meta.height)) return null;
|
|
6861
7003
|
const want = plan.width / plan.height;
|
|
6862
7004
|
const got = meta.width / meta.height;
|
|
6863
7005
|
if (got >= 1 !== want >= 1) return null;
|
|
6864
7006
|
if (meta.width === plan.width && meta.height === plan.height) return engineImage;
|
|
6865
7007
|
const straight = Math.abs(got - want) / want <= 0.02;
|
|
6866
|
-
return
|
|
7008
|
+
return sharp19(engineImage).resize(plan.width, plan.height, { fit: straight ? "fill" : "cover", position: "centre" }).png().toBuffer();
|
|
6867
7009
|
}
|
|
6868
7010
|
async function seamScore(image, plan, source) {
|
|
6869
|
-
const { data, info } = await
|
|
7011
|
+
const { data, info } = await sharp19(image).removeAlpha().greyscale().raw().toBuffer({ resolveWithObject: true });
|
|
6870
7012
|
const W = info.width;
|
|
6871
7013
|
const H = info.height;
|
|
6872
7014
|
const horizontal = plan.axis === "width";
|
|
@@ -6899,7 +7041,7 @@ var SEAM_VISIBLE = 2.2;
|
|
|
6899
7041
|
var OFFSET = 4;
|
|
6900
7042
|
var RESIDUAL_VISIBLE = 15;
|
|
6901
7043
|
async function seamResidual(image, plan, source) {
|
|
6902
|
-
const { data, info } = await
|
|
7044
|
+
const { data, info } = await sharp19(image).removeAlpha().raw().toBuffer({ resolveWithObject: true });
|
|
6903
7045
|
const W = info.width;
|
|
6904
7046
|
const H = info.height;
|
|
6905
7047
|
const ch = info.channels;
|
|
@@ -6934,7 +7076,7 @@ var MAX_SHARE = 0.8;
|
|
|
6934
7076
|
async function subjectFraction(src, source, axis) {
|
|
6935
7077
|
try {
|
|
6936
7078
|
const window = axis === "width" ? { width: Math.max(8, Math.round(source.width * 0.5)), height: source.height } : { width: source.width, height: Math.max(8, Math.round(source.height * 0.5)) };
|
|
6937
|
-
const { info } = await
|
|
7079
|
+
const { info } = await sharp19(src).resize(window.width, window.height, { fit: "cover", position: "attention" }).toBuffer({ resolveWithObject: true });
|
|
6938
7080
|
const offset = axis === "width" ? Math.abs(typeof info.cropOffsetLeft === "number" ? info.cropOffsetLeft : 0) : Math.abs(typeof info.cropOffsetTop === "number" ? info.cropOffsetTop : 0);
|
|
6939
7081
|
const span = axis === "width" ? source.width : source.height;
|
|
6940
7082
|
const extent = axis === "width" ? window.width : window.height;
|
|
@@ -6957,14 +7099,14 @@ function placeExpand(plan, source, fraction) {
|
|
|
6957
7099
|
}
|
|
6958
7100
|
var NEUTRAL = { r: 128, g: 128, b: 128 };
|
|
6959
7101
|
async function conditioningCanvas(source, plan, fill = "edge") {
|
|
6960
|
-
const meta = await
|
|
7102
|
+
const meta = await sharp19(source).metadata();
|
|
6961
7103
|
const sw = meta.width ?? 0;
|
|
6962
7104
|
const sh = meta.height ?? 0;
|
|
6963
7105
|
if (!(sw > 0 && sh > 0)) throw new Error("conditioningCanvas: source has no dimensions");
|
|
6964
7106
|
const layers = [];
|
|
6965
7107
|
if (fill === "edge") layers.push(...await edgeMargins(source, plan, { width: sw, height: sh }));
|
|
6966
7108
|
layers.push({ input: source, left: plan.left, top: plan.top });
|
|
6967
|
-
const canvas =
|
|
7109
|
+
const canvas = sharp19({
|
|
6968
7110
|
create: {
|
|
6969
7111
|
width: plan.width,
|
|
6970
7112
|
height: plan.height,
|
|
@@ -6976,7 +7118,7 @@ async function conditioningCanvas(source, plan, fill = "edge") {
|
|
|
6976
7118
|
}
|
|
6977
7119
|
async function edgeMargins(source, plan, size) {
|
|
6978
7120
|
const out = [];
|
|
6979
|
-
const strip = async (extract, width, height) =>
|
|
7121
|
+
const strip = async (extract, width, height) => sharp19(source).extract(extract).resize(width, height, { fit: "fill" }).png().toBuffer();
|
|
6980
7122
|
if (plan.axis === "width") {
|
|
6981
7123
|
const before = plan.left;
|
|
6982
7124
|
const after = plan.width - plan.left - size.width;
|
|
@@ -7067,12 +7209,12 @@ async function resolveOutpaintRoute(all, shot) {
|
|
|
7067
7209
|
return { engine: shot, method: "reframe", crossed: false };
|
|
7068
7210
|
}
|
|
7069
7211
|
async function driftDiff(a, b) {
|
|
7070
|
-
const metaA = await
|
|
7071
|
-
const metaB = await
|
|
7212
|
+
const metaA = await sharp19(a).metadata();
|
|
7213
|
+
const metaB = await sharp19(b).metadata();
|
|
7072
7214
|
const width = Math.min(metaA.width ?? 1, metaB.width ?? 1, 1024);
|
|
7073
7215
|
const height = Math.min(metaA.height ?? 1, metaB.height ?? 1, 1024);
|
|
7074
7216
|
const [rawA, rawB] = await Promise.all(
|
|
7075
|
-
[a, b].map((buf) =>
|
|
7217
|
+
[a, b].map((buf) => sharp19(buf).resize(width, height, { fit: "cover" }).ensureAlpha().raw().toBuffer())
|
|
7076
7218
|
);
|
|
7077
7219
|
const out = new PNG({ width, height });
|
|
7078
7220
|
const changed = pixelmatch(rawA, rawB, out.data, width, height, { threshold: 0.1, diffColor: [255, 64, 64] });
|
|
@@ -7084,11 +7226,11 @@ async function driftDiff(a, b) {
|
|
|
7084
7226
|
};
|
|
7085
7227
|
}
|
|
7086
7228
|
async function changeMask(a, b, cap2 = 1024) {
|
|
7087
|
-
const metaA = await
|
|
7229
|
+
const metaA = await sharp19(a).metadata();
|
|
7088
7230
|
const width = Math.min(metaA.width ?? 1, cap2);
|
|
7089
7231
|
const height = Math.min(metaA.height ?? 1, cap2);
|
|
7090
7232
|
const [rawA, rawB] = await Promise.all(
|
|
7091
|
-
[a, b].map((buf) =>
|
|
7233
|
+
[a, b].map((buf) => sharp19(buf).resize(width, height, { fit: "fill" }).ensureAlpha().raw().toBuffer())
|
|
7092
7234
|
);
|
|
7093
7235
|
const out = new PNG({ width, height });
|
|
7094
7236
|
pixelmatch(rawA, rawB, out.data, width, height, { threshold: 0.1, includeAA: true, diffMask: true });
|
|
@@ -7137,8 +7279,8 @@ function dilationFor(longEdge) {
|
|
|
7137
7279
|
// src/localEdit.ts
|
|
7138
7280
|
async function preserveOutsideChange(source, edited) {
|
|
7139
7281
|
try {
|
|
7140
|
-
const srcMeta = await
|
|
7141
|
-
const outMeta = await
|
|
7282
|
+
const srcMeta = await sharp19(source).metadata();
|
|
7283
|
+
const outMeta = await sharp19(edited).metadata();
|
|
7142
7284
|
if (!srcMeta.width || !srcMeta.height || !outMeta.width || !outMeta.height)
|
|
7143
7285
|
return { image: edited, outcome: "error", changed: 0 };
|
|
7144
7286
|
const sameShape = Math.abs(outMeta.width / outMeta.height - srcMeta.width / srcMeta.height) / (srcMeta.width / srcMeta.height) <= 0.01;
|
|
@@ -7148,94 +7290,24 @@ async function preserveOutsideChange(source, edited) {
|
|
|
7148
7290
|
if (outcome !== "composited") return { image: edited, outcome, changed: shape.changed };
|
|
7149
7291
|
const r = dilationFor(Math.max(shape.width, shape.height));
|
|
7150
7292
|
const rawShape = { raw: { width: shape.width, height: shape.height, channels: 1 } };
|
|
7151
|
-
const spread = await
|
|
7152
|
-
const dilated = await
|
|
7153
|
-
const feathered = await
|
|
7154
|
-
const grown = await
|
|
7155
|
-
const editedRgb = await
|
|
7156
|
-
const masked = await
|
|
7293
|
+
const spread = await sharp19(shape.mask, rawShape).blur(r).toColourspace("b-w").raw().toBuffer();
|
|
7294
|
+
const dilated = await sharp19(spread, rawShape).threshold(8).toColourspace("b-w").raw().toBuffer();
|
|
7295
|
+
const feathered = await sharp19(dilated, rawShape).blur(Math.max(2, r / 3)).toColourspace("b-w").raw().toBuffer();
|
|
7296
|
+
const grown = await sharp19(feathered, rawShape).resize(srcMeta.width, srcMeta.height, { fit: "fill", kernel: "cubic" }).toColourspace("b-w").raw().toBuffer();
|
|
7297
|
+
const editedRgb = await sharp19(edited).resize(srcMeta.width, srcMeta.height, { fit: "fill" }).removeAlpha().raw().toBuffer();
|
|
7298
|
+
const masked = await sharp19(editedRgb, {
|
|
7157
7299
|
raw: { width: srcMeta.width, height: srcMeta.height, channels: 3 }
|
|
7158
7300
|
}).joinChannel(grown, { raw: { width: srcMeta.width, height: srcMeta.height, channels: 1 } }).png().toBuffer();
|
|
7159
|
-
const image = await
|
|
7301
|
+
const image = await sharp19(source).removeAlpha().composite([{ input: masked }]).png().toBuffer();
|
|
7160
7302
|
return { image, outcome: "composited", changed: shape.changed };
|
|
7161
7303
|
} catch {
|
|
7162
7304
|
return { image: edited, outcome: "error", changed: 0 };
|
|
7163
7305
|
}
|
|
7164
7306
|
}
|
|
7165
|
-
function joinNames(labels) {
|
|
7166
|
-
const uniq = [...new Set(labels)];
|
|
7167
|
-
if (uniq.length <= 1) return uniq[0] ?? "";
|
|
7168
|
-
return `${uniq.slice(0, -1).join(", ")} and ${uniq[uniq.length - 1]}`;
|
|
7169
|
-
}
|
|
7170
|
-
function brandContext(core, brandId) {
|
|
7171
|
-
const brand = core.store.getBrand(brandId);
|
|
7172
|
-
if (!brand) throw new Error("brand not found");
|
|
7173
|
-
const assetPaths = {};
|
|
7174
|
-
const json = brand.json;
|
|
7175
|
-
for (const logo of json.logos ?? []) {
|
|
7176
|
-
const ref = String(logo.file ?? "");
|
|
7177
|
-
if (ref.startsWith("asset:") && core.images.has(ref.slice(6))) assetPaths[ref] = core.images.pathFor(ref.slice(6));
|
|
7178
|
-
}
|
|
7179
|
-
return { brand: brand.json, assetPaths };
|
|
7180
|
-
}
|
|
7181
|
-
var PNG_SIG = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
7182
|
-
var assetHash2 = (ref) => {
|
|
7183
|
-
const s = String(ref ?? "");
|
|
7184
|
-
return s.startsWith("asset:") ? s.slice(6) : null;
|
|
7185
|
-
};
|
|
7186
|
-
var LOGO_ROLES = ["primary", "mark", "wordmark", "monochrome", "alternate"];
|
|
7187
|
-
var LOGO_BACKGROUNDS = ["light", "dark", "any"];
|
|
7188
|
-
var toPng = (buf) => sharp7(buf).rotate().png().toBuffer();
|
|
7189
|
-
var COST_PROBE = {
|
|
7190
|
-
prompt: "",
|
|
7191
|
-
brand: { brand: {}, assetPaths: {} },
|
|
7192
|
-
width: 1024,
|
|
7193
|
-
height: 1024,
|
|
7194
|
-
count: 1
|
|
7195
|
-
};
|
|
7196
|
-
var MARK_MAX_EDGE = 2048;
|
|
7197
|
-
var toMarkPng = (buf) => sharp7(buf, { density: 384 }).rotate().resize({ width: MARK_MAX_EDGE, height: MARK_MAX_EDGE, fit: "inside", withoutEnlargement: true }).png().toBuffer();
|
|
7198
|
-
var cappedRefs = /* @__PURE__ */ new Map();
|
|
7199
|
-
async function capReferenceEdge(core, path, maxEdge) {
|
|
7200
|
-
const key = `${path}#${maxEdge}`;
|
|
7201
|
-
const hit = cappedRefs.get(key);
|
|
7202
|
-
if (hit) return hit;
|
|
7203
|
-
let out = path;
|
|
7204
|
-
try {
|
|
7205
|
-
const meta = await sharp7(path).metadata();
|
|
7206
|
-
if ((meta.width ?? 0) > maxEdge || (meta.height ?? 0) > maxEdge) {
|
|
7207
|
-
const buf = await sharp7(path).resize({ width: maxEdge, height: maxEdge, fit: "inside", withoutEnlargement: true }).png().toBuffer();
|
|
7208
|
-
out = core.images.pathFor(core.images.save(buf));
|
|
7209
|
-
}
|
|
7210
|
-
} catch {
|
|
7211
|
-
}
|
|
7212
|
-
cappedRefs.set(key, out);
|
|
7213
|
-
return out;
|
|
7214
|
-
}
|
|
7215
|
-
var readImagePart = async (core, req, normalize2) => {
|
|
7216
|
-
const part = await req.file();
|
|
7217
|
-
if (!part) return { error: "multipart file field required" };
|
|
7218
|
-
const buf = await part.toBuffer();
|
|
7219
|
-
if (buf.length === 0) return { error: "empty file" };
|
|
7220
|
-
try {
|
|
7221
|
-
return { hash: core.images.save(await normalize2(buf)), fields: part.fields ?? {}, filename: part.filename };
|
|
7222
|
-
} catch {
|
|
7223
|
-
return { error: "that file is not an image we can read" };
|
|
7224
|
-
}
|
|
7225
|
-
};
|
|
7226
|
-
var mtimeQS = (path) => existsSync(path) ? `?v=${Math.round(statSync(path).mtimeMs)}` : "";
|
|
7227
|
-
var serveJpeg = (req, reply, path) => {
|
|
7228
|
-
const etag = `"${statSync(path).mtimeMs}"`;
|
|
7229
|
-
reply.header("cache-control", "public, max-age=31536000, immutable").header("etag", etag);
|
|
7230
|
-
if (req.headers["if-none-match"] === etag) return reply.status(304).send();
|
|
7231
|
-
return reply.header("content-type", "image/jpeg").send(readFileSync(path));
|
|
7232
|
-
};
|
|
7233
|
-
|
|
7234
|
-
// src/routes/logos.ts
|
|
7235
7307
|
function registerLogoRoutes(app, deps) {
|
|
7236
7308
|
const { core } = deps;
|
|
7237
7309
|
const readLogos = (json) => Array.isArray(json.logos) ? json.logos : [];
|
|
7238
|
-
const findLogo = (json, hash) => readLogos(json).findIndex((l) =>
|
|
7310
|
+
const findLogo = (json, hash) => readLogos(json).findIndex((l) => assetHash(l?.file) === hash);
|
|
7239
7311
|
const enumField = (raw, allowed, fallback) => {
|
|
7240
7312
|
const v = raw === void 0 || raw === null ? "" : String(raw);
|
|
7241
7313
|
if (!v) return fallback ?? "";
|
|
@@ -7248,7 +7320,7 @@ function registerLogoRoutes(app, deps) {
|
|
|
7248
7320
|
const logos = [...readLogos(json)];
|
|
7249
7321
|
const part = await readImagePart(core, req, toMarkPng);
|
|
7250
7322
|
if ("error" in part) return reply.status(400).send({ error: part.error });
|
|
7251
|
-
const existing = logos.findIndex((l) =>
|
|
7323
|
+
const existing = logos.findIndex((l) => assetHash(l?.file) === part.hash);
|
|
7252
7324
|
const role = enumField(
|
|
7253
7325
|
part.fields?.role?.value,
|
|
7254
7326
|
LOGO_ROLES,
|
|
@@ -7263,7 +7335,10 @@ function registerLogoRoutes(app, deps) {
|
|
|
7263
7335
|
json.logos = role === "primary" ? logos.map((l) => l !== entry && l?.role === "primary" ? { ...l, role: "alternate" } : l) : logos;
|
|
7264
7336
|
const v = validateBrand(json);
|
|
7265
7337
|
if (!v.valid) return reply.status(400).send({ error: "brand became invalid", details: v.errors });
|
|
7266
|
-
|
|
7338
|
+
const row = core.store.updateBrand(brand.id, json);
|
|
7339
|
+
const meta = await sharp19(core.images.read(part.hash)).metadata().catch(() => null);
|
|
7340
|
+
const logoEdge = meta ? Math.max(meta.width ?? 0, meta.height ?? 0) || null : null;
|
|
7341
|
+
return { ...row, logoHash: part.hash, logoEdge };
|
|
7267
7342
|
});
|
|
7268
7343
|
app.patch("/api/brands/:id/logos/:hash", async (req, reply) => {
|
|
7269
7344
|
const brand = core.store.getBrand(req.params.id);
|
|
@@ -7388,7 +7463,7 @@ async function vibrantColor(input) {
|
|
|
7388
7463
|
let data;
|
|
7389
7464
|
let channels;
|
|
7390
7465
|
try {
|
|
7391
|
-
const out = await
|
|
7466
|
+
const out = await sharp19(input).resize(48, 48, { fit: "inside" }).raw().toBuffer({ resolveWithObject: true });
|
|
7392
7467
|
data = out.data;
|
|
7393
7468
|
channels = out.info.channels;
|
|
7394
7469
|
} catch {
|
|
@@ -7411,7 +7486,7 @@ async function vibrantColor(input) {
|
|
|
7411
7486
|
const best = buckets.reduce((a, b) => b.score > a.score ? b : a, buckets[0]);
|
|
7412
7487
|
if (best.score <= 0) {
|
|
7413
7488
|
try {
|
|
7414
|
-
const { dominant } = await
|
|
7489
|
+
const { dominant } = await sharp19(input).stats();
|
|
7415
7490
|
return toHex(dominant.r, dominant.g, dominant.b);
|
|
7416
7491
|
} catch {
|
|
7417
7492
|
return null;
|
|
@@ -8029,7 +8104,7 @@ async function buildExportZip(image, baseName, presetIds) {
|
|
|
8029
8104
|
const chosen = EXPORT_PRESETS.filter((p) => presetIds.includes(p.id));
|
|
8030
8105
|
if (chosen.length === 0) throw new Error("No valid export presets selected");
|
|
8031
8106
|
for (const p of chosen) {
|
|
8032
|
-
const buf = p.width && p.height ? await
|
|
8107
|
+
const buf = p.width && p.height ? await sharp19(image).resize(p.width, p.height, { fit: "cover", position: "attention" }).png().toBuffer() : image;
|
|
8033
8108
|
zip.file(`${baseName}-${p.id}.png`, buf);
|
|
8034
8109
|
}
|
|
8035
8110
|
return zip.generateAsync({ type: "nodebuffer" });
|
|
@@ -8190,7 +8265,8 @@ function registerImageRoutes(app, deps) {
|
|
|
8190
8265
|
if (!part) return reply.status(400).send({ error: "multipart file field required" });
|
|
8191
8266
|
const buf = await part.toBuffer();
|
|
8192
8267
|
if (buf.length === 0) return reply.status(400).send({ error: "empty file" });
|
|
8193
|
-
const
|
|
8268
|
+
const fmt = (await sharp19(buf).metadata().catch(() => null))?.format;
|
|
8269
|
+
const png = fmt === "svg" ? await toMarkPng(buf) : await sharp19(buf).rotate().png().toBuffer();
|
|
8194
8270
|
return { hash: core.images.save(png) };
|
|
8195
8271
|
});
|
|
8196
8272
|
app.post("/api/diff", async (req, reply) => {
|
|
@@ -8225,6 +8301,24 @@ function registerImageRoutes(app, deps) {
|
|
|
8225
8301
|
|
|
8226
8302
|
// src/release/notes.data.ts
|
|
8227
8303
|
var RELEASES = [
|
|
8304
|
+
{
|
|
8305
|
+
version: "0.6.13",
|
|
8306
|
+
date: "2026-08-30",
|
|
8307
|
+
sections: [
|
|
8308
|
+
{
|
|
8309
|
+
heading: "Products",
|
|
8310
|
+
body: "A product sold in several colours renders the colour its first photo shows, instead of blending them. Imported products now carry their own description and colour list into every shot, so scale and colourway stay true."
|
|
8311
|
+
},
|
|
8312
|
+
{
|
|
8313
|
+
heading: "Brand",
|
|
8314
|
+
body: "Attach your own logo straight from the Brand tab when composing; it joins the kit and rides the shot. Small logo files are raised to a workable resolution, the smallest lettering and its script are now part of the contract, and a logo too small to survive says so before you spend."
|
|
8315
|
+
},
|
|
8316
|
+
{
|
|
8317
|
+
heading: "Create",
|
|
8318
|
+
body: "The refine strip counts each product once and names the scene a thread keeps. A retried shot carries your recipe, never leftovers from the previous run."
|
|
8319
|
+
}
|
|
8320
|
+
]
|
|
8321
|
+
},
|
|
8228
8322
|
{
|
|
8229
8323
|
version: "0.6.12",
|
|
8230
8324
|
date: "2026-08-29",
|
|
@@ -9002,6 +9096,12 @@ function buildServer(opts) {
|
|
|
9002
9096
|
// about its own format — broken in the marks grid, and mislabelled to any
|
|
9003
9097
|
// engine it is later attached to.
|
|
9004
9098
|
saveAsset: async (buf) => `asset:${core.images.save(await toMarkPng(buf))}`,
|
|
9099
|
+
// Measured as stored (post-toMarkPng), so the scrape judges the same
|
|
9100
|
+
// pixels the compiler will one day attach.
|
|
9101
|
+
probeLongEdge: async (buf) => {
|
|
9102
|
+
const m = await sharp19(await toMarkPng(buf)).metadata();
|
|
9103
|
+
return Math.max(m.width ?? 0, m.height ?? 0) || null;
|
|
9104
|
+
},
|
|
9005
9105
|
createdWith: `${meta.name}/${meta.version}`
|
|
9006
9106
|
});
|
|
9007
9107
|
const row = core.store.createBrand(brand);
|
|
@@ -9087,6 +9187,10 @@ function buildServer(opts) {
|
|
|
9087
9187
|
const { brand: scraped, warnings } = await buildFromUrl(url, {
|
|
9088
9188
|
fetchImpl: opts.fetchImpl,
|
|
9089
9189
|
saveAsset: async (buf) => `asset:${core.images.save(await toMarkPng(buf))}`,
|
|
9190
|
+
probeLongEdge: async (buf) => {
|
|
9191
|
+
const m = await sharp19(await toMarkPng(buf)).metadata();
|
|
9192
|
+
return Math.max(m.width ?? 0, m.height ?? 0) || null;
|
|
9193
|
+
},
|
|
9090
9194
|
createdWith: `${meta.name}/${meta.version}`
|
|
9091
9195
|
});
|
|
9092
9196
|
const { brand: merged, suggestions } = mergeScrape(brand.json, scraped);
|
|
@@ -9189,13 +9293,17 @@ function buildServer(opts) {
|
|
|
9189
9293
|
registerDemoProductRoutes(app, { templatesRoot, demoProducts, demoProductById });
|
|
9190
9294
|
registerShowcaseRoutes(app, { templatesRoot });
|
|
9191
9295
|
app.get("/api/formats", async () => FORMATS);
|
|
9296
|
+
function briefInputsOnly(brief) {
|
|
9297
|
+
const { inherited, rendered, croppedFrom, resizedFrom, resampledHops, expand, crop: crop2, sourceImage, ...inputs } = brief;
|
|
9298
|
+
return inputs;
|
|
9299
|
+
}
|
|
9192
9300
|
async function compileEditBrief(brandId, parentId, brief, engineCaps, opts2) {
|
|
9193
9301
|
const inherited = inheritedIdentityTokens(parentId, (id) => core.store.getNode(id));
|
|
9194
9302
|
const borrowed = inherited.tokens;
|
|
9195
9303
|
const already = new Set(
|
|
9196
|
-
brief.tokens.filter((t) => t.t === "product" || t.t === "character" || t.t === "mark" || t.t === "ref").map(
|
|
9304
|
+
brief.tokens.filter((t) => t.t === "product" || t.t === "character" || t.t === "mark" || t.t === "ref").map(identityTokenKey)
|
|
9197
9305
|
);
|
|
9198
|
-
const inheritedTokens = borrowed.filter((t) => !already.has(
|
|
9306
|
+
const inheritedTokens = borrowed.filter((t) => !already.has(identityTokenKey(t)));
|
|
9199
9307
|
const combined = [...brief.tokens, ...inheritedTokens];
|
|
9200
9308
|
const brandJson = await brandJsonWithResolvedPresenters(
|
|
9201
9309
|
core,
|
|
@@ -9412,11 +9520,11 @@ function buildServer(opts) {
|
|
|
9412
9520
|
const out = [];
|
|
9413
9521
|
for (const h of images) {
|
|
9414
9522
|
const buf = core.images.read(h);
|
|
9415
|
-
const meta2 = await
|
|
9523
|
+
const meta2 = await sharp19(buf).metadata().catch(() => null);
|
|
9416
9524
|
if (!meta2?.width || !meta2.height) throw new Error("engine returned an undecodable image");
|
|
9417
9525
|
const oriented = (meta2.orientation ?? 1) !== 1;
|
|
9418
9526
|
out.push(
|
|
9419
|
-
buf.subarray(0, 8).equals(PNG_SIG) && !oriented ? h : core.images.save(await
|
|
9527
|
+
buf.subarray(0, 8).equals(PNG_SIG) && !oriented ? h : core.images.save(await sharp19(buf).rotate().png().toBuffer())
|
|
9420
9528
|
);
|
|
9421
9529
|
}
|
|
9422
9530
|
return out;
|
|
@@ -9428,7 +9536,7 @@ function buildServer(opts) {
|
|
|
9428
9536
|
const out = [];
|
|
9429
9537
|
for (const h of images) {
|
|
9430
9538
|
const buf = core.images.read(h);
|
|
9431
|
-
const meta2 = await
|
|
9539
|
+
const meta2 = await sharp19(buf).metadata();
|
|
9432
9540
|
if (!meta2.width || !meta2.height) {
|
|
9433
9541
|
out.push(h);
|
|
9434
9542
|
continue;
|
|
@@ -9441,7 +9549,7 @@ function buildServer(opts) {
|
|
|
9441
9549
|
}
|
|
9442
9550
|
const w = got > target ? Math.round(meta2.height * target) : meta2.width;
|
|
9443
9551
|
const hpx = got > target ? meta2.height : Math.round(meta2.width / target);
|
|
9444
|
-
const cropped = await
|
|
9552
|
+
const cropped = await sharp19(buf).resize(w, hpx, { fit: "cover", position: "attention" }).png().toBuffer();
|
|
9445
9553
|
app.log.info(
|
|
9446
9554
|
{ nodeId, got: `${meta2.width}x${meta2.height}`, want: `${w}x${hpx}` },
|
|
9447
9555
|
"canvas: cropped a drifted frame to the asked ratio"
|
|
@@ -9460,7 +9568,7 @@ function buildServer(opts) {
|
|
|
9460
9568
|
async function assertAspect(images, expect) {
|
|
9461
9569
|
const want = expect.width / expect.height;
|
|
9462
9570
|
for (const h of images) {
|
|
9463
|
-
const meta2 = await
|
|
9571
|
+
const meta2 = await sharp19(core.images.read(h)).metadata();
|
|
9464
9572
|
if (!meta2.width || !meta2.height) continue;
|
|
9465
9573
|
const got = meta2.width / meta2.height;
|
|
9466
9574
|
if (Math.abs(got - want) / want > ASPECT_TOLERANCE)
|
|
@@ -9492,7 +9600,7 @@ function buildServer(opts) {
|
|
|
9492
9600
|
try {
|
|
9493
9601
|
const sizes = [];
|
|
9494
9602
|
for (const h of result.images) {
|
|
9495
|
-
const meta2 = await
|
|
9603
|
+
const meta2 = await sharp19(core.images.read(h)).metadata();
|
|
9496
9604
|
if (meta2.width && meta2.height) sizes.push([meta2.width, meta2.height]);
|
|
9497
9605
|
}
|
|
9498
9606
|
const node = core.store.getNode(nodeId);
|
|
@@ -9559,7 +9667,7 @@ function buildServer(opts) {
|
|
|
9559
9667
|
if (!srcHash || !core.images.has(String(srcHash)))
|
|
9560
9668
|
return reply.status(400).send({ error: "edit needs a parent node with an image (sourceImage)" });
|
|
9561
9669
|
const srcBuf = core.images.read(String(srcHash));
|
|
9562
|
-
const srcMeta = await
|
|
9670
|
+
const srcMeta = await sharp19(srcBuf).metadata();
|
|
9563
9671
|
if (!srcMeta.width || !srcMeta.height) return reply.status(400).send({ error: "source image unreadable" });
|
|
9564
9672
|
const plan2 = planCrop({ width: srcMeta.width, height: srcMeta.height }, Number(fmt.w) / Number(fmt.h));
|
|
9565
9673
|
if (!plan2) return reply.status(400).send({ error: "the picture is already this shape" });
|
|
@@ -9576,13 +9684,13 @@ function buildServer(opts) {
|
|
|
9576
9684
|
engineId: "local"
|
|
9577
9685
|
});
|
|
9578
9686
|
core.store.setBrief(node2.id, {
|
|
9579
|
-
...brief ?? {},
|
|
9687
|
+
...briefInputsOnly(brief ?? {}),
|
|
9580
9688
|
sourceImage: String(srcHash),
|
|
9581
9689
|
reshape: "crop",
|
|
9582
9690
|
crop: window
|
|
9583
9691
|
});
|
|
9584
9692
|
const work2 = async () => ({
|
|
9585
|
-
images: [core.images.save(await
|
|
9693
|
+
images: [core.images.save(await sharp19(srcBuf).extract(window).png().toBuffer())],
|
|
9586
9694
|
costUsd: 0
|
|
9587
9695
|
});
|
|
9588
9696
|
void runNode(node2.id, null, 0, work2, { width: plan2.width, height: plan2.height }).catch(
|
|
@@ -9734,7 +9842,7 @@ function buildServer(opts) {
|
|
|
9734
9842
|
const editEdge = engine.capabilities().maxReferenceEdge;
|
|
9735
9843
|
if (editEdge) for (const r of editRefs) r.path = await capReferenceEdge(core, r.path, editEdge);
|
|
9736
9844
|
const srcBuf = core.images.read(String(srcHash));
|
|
9737
|
-
const srcMeta = await
|
|
9845
|
+
const srcMeta = await sharp19(srcBuf).metadata();
|
|
9738
9846
|
if (srcMeta.width && srcMeta.height) expectShape = { width: srcMeta.width, height: srcMeta.height };
|
|
9739
9847
|
const parentFormat = parent?.brief?.tokens?.find((t) => t?.t === "format");
|
|
9740
9848
|
const parentNominal = parentFormat && Number(parentFormat.w) > 0 && Number(parentFormat.h) > 0 ? { width: Number(parentFormat.w), height: Number(parentFormat.h) } : null;
|
|
@@ -9854,7 +9962,7 @@ function buildServer(opts) {
|
|
|
9854
9962
|
});
|
|
9855
9963
|
if (brief)
|
|
9856
9964
|
core.store.setBrief(node.id, {
|
|
9857
|
-
...brief,
|
|
9965
|
+
...briefInputsOnly(brief),
|
|
9858
9966
|
...editedFrom ? { sourceImage: editedFrom } : {},
|
|
9859
9967
|
...kind === "edit" && reshape ? { reshape } : {},
|
|
9860
9968
|
// How the margin was actually made, and by whom. An extend may be
|
|
@@ -9879,11 +9987,11 @@ function buildServer(opts) {
|
|
|
9879
9987
|
const original = editedFrom ? core.images.read(editedFrom) : null;
|
|
9880
9988
|
const localScope = kind === "edit" && !plan && editScope === "local" && original;
|
|
9881
9989
|
const enforceEditCanvas = async (images) => {
|
|
9882
|
-
const srcMeta = await
|
|
9990
|
+
const srcMeta = await sharp19(original).metadata();
|
|
9883
9991
|
if (!srcMeta.width || !srcMeta.height) return images;
|
|
9884
9992
|
const out = [];
|
|
9885
9993
|
for (const h of images) {
|
|
9886
|
-
const meta2 = await
|
|
9994
|
+
const meta2 = await sharp19(core.images.read(h)).metadata();
|
|
9887
9995
|
const got = { width: meta2.width ?? 0, height: meta2.height ?? 0 };
|
|
9888
9996
|
const verdict = judgeEditSize({ width: srcMeta.width, height: srcMeta.height }, got);
|
|
9889
9997
|
if (verdict.action === "reject")
|
|
@@ -9897,7 +10005,7 @@ function buildServer(opts) {
|
|
|
9897
10005
|
);
|
|
9898
10006
|
out.push(
|
|
9899
10007
|
core.images.save(
|
|
9900
|
-
await
|
|
10008
|
+
await sharp19(core.images.read(h)).resize(srcMeta.width, srcMeta.height, { fit: "fill" }).png().toBuffer()
|
|
9901
10009
|
)
|
|
9902
10010
|
);
|
|
9903
10011
|
try {
|
|
@@ -9919,7 +10027,7 @@ function buildServer(opts) {
|
|
|
9919
10027
|
const out = [];
|
|
9920
10028
|
for (const h of images) {
|
|
9921
10029
|
const answer = core.images.read(h);
|
|
9922
|
-
const got = await
|
|
10030
|
+
const got = await sharp19(answer).metadata();
|
|
9923
10031
|
if (got.width !== plan.width || got.height !== plan.height)
|
|
9924
10032
|
app.log.info(
|
|
9925
10033
|
{ nodeId: node.id, got: `${got.width}x${got.height}`, want: `${plan.width}x${plan.height}` },
|
|
@@ -10220,8 +10328,8 @@ async function verify() {
|
|
|
10220
10328
|
const db = new Database2(":memory:");
|
|
10221
10329
|
db.pragma("user_version");
|
|
10222
10330
|
db.close();
|
|
10223
|
-
const { default:
|
|
10224
|
-
await
|
|
10331
|
+
const { default: sharp20 } = await import('sharp');
|
|
10332
|
+
await sharp20({ create: { width: 1, height: 1, channels: 3, background: "#000" } }).png().toBuffer();
|
|
10225
10333
|
console.log(JSON.stringify({ ok: true, version: readMeta().version }));
|
|
10226
10334
|
} catch (err) {
|
|
10227
10335
|
console.log(JSON.stringify({ ok: false, error: String(err?.message ?? err) }));
|