@hraness/slopcamera 3.4.0 → 3.6.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.
- package/README.md +32 -19
- package/apps/desktop/application/operation.ts +1 -1
- package/apps/desktop/cli/args.ts +3 -1
- package/apps/desktop/cli/commands.ts +22 -1
- package/apps/desktop/cli/help.ts +37 -13
- package/apps/desktop/cli/menubar-status.ts +155 -0
- package/apps/desktop/cli/menubar.ts +129 -101
- package/apps/desktop/dist/cli/main.js +334 -312
- package/apps/desktop/html-overlay/catalog.ts +1 -0
- package/apps/desktop/html-overlay/spatial.ts +10 -6
- package/apps/desktop/workflows/index.ts +1 -0
- package/dist/cli.js +7 -8
- package/dist/generate.js +1 -1
- package/dist/{index-qtn3s421.js → index-4z9y2wa2.js} +1 -1
- package/dist/{index-sy1n4zfy.js → index-fevf4ppk.js} +1 -1
- package/dist/{index-qry58nj2.js → index-mnp0bftq.js} +2 -2
- package/dist/index-nxtmy18t.js +3 -0
- package/dist/{index-abnk4h9c.js → index-qxfexvj3.js} +2 -2
- package/dist/index.js +1 -1
- package/dist/operations.js +1 -1
- package/dist/vectorize/worker.js +1 -1
- package/dist/workflow.js +1 -1
- package/docs/README.md +3 -3
- package/examples/studio/vgpu/README.md +1 -1
- package/package.json +5 -6
- package/skills/slopcamera/SKILL.md +4 -3
- package/skills/slopcamera/references/brand-illustrations.md +237 -0
- package/skills/slopcamera/references/gateway-media.md +1 -1
- package/skills/slopcamera/references/install.md +10 -1
- package/skills/slopcamera/references/patent-drawings.md +4 -4
- package/skills/slopcamera/references/support.md +1 -1
- package/skills/slopcamera/references/vectorization.md +3 -1
- package/src/cli.ts +13 -10
- package/src/code/public-operations.ts +9 -0
- package/src/generate.ts +44 -1
- package/src/generation-pricing.ts +217 -0
- package/src/mcp/tools.ts +4 -0
- package/src/operations.ts +4 -0
- package/src/support.ts +7 -1
- package/src/vectorize/command.ts +6 -1
- package/src/vectorize/supervisor.ts +23 -3
- package/src/vectorize/tool.ts +8 -4
- package/src/version.ts +1 -1
- package/src/visual-style.ts +4 -1
- package/dist/index-77fjfg9f.js +0 -3
|
@@ -931,11 +931,13 @@ export function createSpatialOverlayBatch(input: unknown) {
|
|
|
931
931
|
return Object.freeze({ authoring: Object.freeze(authoring), metadata: metadata.value as unknown as DeepReadonly<typeof metadataValue>, metadataSha256: metadata.sha256 });
|
|
932
932
|
}
|
|
933
933
|
|
|
934
|
-
/** Fixed golden-spiral gather taps; computed on the host
|
|
934
|
+
/** Fixed golden-spiral gather taps and bloom weights; computed on the host and
|
|
935
|
+
* bound as uniform arrays because the overlay compiles GLSL ES 1.00, which has
|
|
936
|
+
* no array constructors. Generated shaders contain no transcendentals. */
|
|
935
937
|
const DOF_TAPS = Object.freeze(Array.from({ length: 16 }, (_, index) => {
|
|
936
938
|
const angle = index * 2.399_963_229_728_653;
|
|
937
939
|
const radius = Math.sqrt((index + 0.5) / 16);
|
|
938
|
-
return `
|
|
940
|
+
return `new THREE.Vector2(${(Math.cos(angle) * radius).toFixed(7)},${(Math.sin(angle) * radius).toFixed(7)})`;
|
|
939
941
|
}).join(","));
|
|
940
942
|
const BLOOM_WEIGHTS = Object.freeze([0.2270270270, 0.1945945946, 0.1216216216, 0.0540540541, 0.0162162162].map(weight => weight.toFixed(10)).join(","));
|
|
941
943
|
|
|
@@ -1113,6 +1115,7 @@ function makeMaterial(object,frame,track){
|
|
|
1113
1115
|
}
|
|
1114
1116
|
const material=track(m.kind==="unlit"?new THREE.MeshBasicMaterial(options):new THREE.MeshStandardMaterial({...options,roughness:m.roughness,metalness:m.metalness}));
|
|
1115
1117
|
if(object.linearColor)material.color.setRGB(...object.linearColor,THREE.LinearSRGBColorSpace);
|
|
1118
|
+
if(m.kind==="standard"&&m.emissive){material.emissive=new THREE.Color(m.emissive.color);material.emissiveIntensity=m.emissive.intensity;}
|
|
1116
1119
|
return material;
|
|
1117
1120
|
}
|
|
1118
1121
|
function makeCamera(data){
|
|
@@ -1170,16 +1173,16 @@ const buildStepPasses=(step,frame,index,track,read)=>{
|
|
|
1170
1173
|
case "vignette":return[{target:"write",material:track(postMaterial(postHeader+"uniform float intensity;uniform float radius;void main(){vec4 c=texture2D(source,sampleUv);float d=length((sampleUv-0.5)*1.41421356);gl_FragColor=vec4(c.rgb*(1.0-intensity*smoothstep(radius,1.0,d)),c.a);}",u({intensity:{value:step.intensity},radius:{value:step.radius}})))}];
|
|
1171
1174
|
case "chromatic-aberration":return[{target:"write",material:track(postMaterial(postHeader+"uniform float offsetPixels;uniform float radialFalloff;void main(){vec2 dir=sampleUv-0.5;float len=max(length(dir),0.000001);vec2 off=dir/len*offsetPixels*pow(len*1.41421356,radialFalloff)/resolution;vec4 c=texture2D(source,sampleUv);gl_FragColor=vec4(texture2D(source,sampleUv+off).r,c.g,texture2D(source,sampleUv-off).b,c.a);}",u({offsetPixels:{value:step.offsetPixels},radialFalloff:{value:step.radialFalloff}})))}];
|
|
1172
1175
|
case "grain":return[{target:"write",material:track(postMaterial(postHeader+"uniform float intensity;uniform float seed;float grainHash(vec2 p){vec3 q=fract(vec3(p.xyx)*0.1031);q+=dot(q,q.yzx+33.33);return fract((q.x+q.y)*q.z);}void main(){vec4 c=texture2D(source,sampleUv);float n=grainHash(floor(sampleUv*resolution)+vec2(seed,seed*1.618034));gl_FragColor=vec4(max(c.rgb+vec3((n-0.5)*intensity),vec3(0.0)),c.a);}",u({intensity:{value:step.intensity},seed:{value:step.seed+frame.timeUs%7919}})))}];
|
|
1173
|
-
case "depth-of-field":return[{target:"write",material:track(postMaterial(postHeader+"uniform sampler2D depthTex;uniform float nearClip;uniform float farClip;uniform float focusDistance;uniform float aperture;uniform float focalLength;uniform float sensorWidth;uniform bool isPerspective;
|
|
1176
|
+
case "depth-of-field":return[{target:"write",material:track(postMaterial(postHeader+"uniform sampler2D depthTex;uniform float nearClip;uniform float farClip;uniform float focusDistance;uniform float aperture;uniform float focalLength;uniform float sensorWidth;uniform bool isPerspective;uniform vec2 dofTaps[16];float viewZ(float d){return isPerspective?nearClip*farClip/max(farClip-d*(farClip-nearClip),0.000001):nearClip+d*(farClip-nearClip);}void main(){vec4 c=texture2D(source,sampleUv);float z=viewZ(texture2D(depthTex,sampleUv).x);float coc=clamp(aperture*focalLength*abs(focusDistance-z)/(focusDistance*max(z,0.0001))*resolution.x/sensorWidth,0.0,32.0);vec3 acc=c.rgb;for(int i=0;i<16;i++){acc+=texture2D(source,sampleUv+dofTaps[i]*coc/resolution).rgb;}gl_FragColor=vec4(acc/17.0,c.a);}",u({depthTex:{value:beautyTarget.depthTexture},nearClip:{value:frame.camera.near},farClip:{value:frame.camera.far},focusDistance:{value:step.focusDistance},aperture:{value:step.aperture},focalLength:{value:step.focalLength},sensorWidth:{value:frame.camera.lens?frame.camera.lens.sensorWidthMm:36},isPerspective:{value:frame.camera.kind==="perspective"},dofTaps:{value:[${DOF_TAPS}]}})))}];
|
|
1174
1177
|
case "motion-blur":return[{target:"write",material:track(postMaterial(postHeader+"uniform sampler2D velocityTex;uniform int samples;uniform float shutter;void main(){vec4 c=texture2D(source,sampleUv);vec2 v=texture2D(velocityTex,sampleUv).xy*shutter/resolution;vec3 acc=c.rgb;float n=1.0;for(int i=1;i<64;i++){if(i>=samples)break;float t=float(i)/float(samples-1)-0.5;acc+=texture2D(source,sampleUv+v*t).rgb;n+=1.0;}gl_FragColor=vec4(acc/n,c.a);}",u({velocityTex:{value:velocityTarget.texture},samples:{value:step.samples},shutter:{value:step.shutterAngle/360}})))}];
|
|
1175
1178
|
case "bloom":{
|
|
1176
1179
|
const bright=postHeader+"uniform float threshold;void main(){vec4 c=texture2D(source,sampleUv);gl_FragColor=vec4(max(c.rgb-vec3(threshold),vec3(0.0)),1.0);}";
|
|
1177
|
-
const blur=postHeader+"uniform vec2 texel;uniform float spread;uniform vec2 direction;
|
|
1180
|
+
const blur=postHeader+"uniform vec2 texel;uniform float spread;uniform vec2 direction;uniform float bloomW[5];void main(){vec3 acc=texture2D(source,sampleUv).rgb*bloomW[0];for(int i=1;i<5;i++){vec2 o=direction*float(i)*spread*0.125*texel;acc+=(texture2D(source,sampleUv+o).rgb+texture2D(source,sampleUv-o).rgb)*bloomW[i];}gl_FragColor=vec4(acc,1.0);}";
|
|
1178
1181
|
const composite=postHeader+"uniform sampler2D bright;uniform float intensity;void main(){vec4 c=texture2D(source,sampleUv);gl_FragColor=vec4(c.rgb+texture2D(bright,sampleUv).rgb*intensity,c.a);}";
|
|
1179
1182
|
const halfTexel=new THREE.Vector2(1/brightA.width,1/brightA.height);
|
|
1180
1183
|
return[{target:brightA,material:track(postMaterial(bright,u({threshold:{value:step.threshold}})))},
|
|
1181
|
-
{target:brightB,material:track(postMaterial(blur,{source:{value:brightA.texture},resolution:{value:resolution},texel:{value:halfTexel},spread:{value:step.radius},direction:{value:new THREE.Vector2(1,0)}}))},
|
|
1182
|
-
{target:brightA,material:track(postMaterial(blur,{source:{value:brightB.texture},resolution:{value:resolution},texel:{value:halfTexel},spread:{value:step.radius},direction:{value:new THREE.Vector2(0,1)}}))},
|
|
1184
|
+
{target:brightB,material:track(postMaterial(blur,{source:{value:brightA.texture},resolution:{value:resolution},texel:{value:halfTexel},spread:{value:step.radius},direction:{value:new THREE.Vector2(1,0)},bloomW:{value:[${BLOOM_WEIGHTS}]}}))},
|
|
1185
|
+
{target:brightA,material:track(postMaterial(blur,{source:{value:brightB.texture},resolution:{value:resolution},texel:{value:halfTexel},spread:{value:step.radius},direction:{value:new THREE.Vector2(0,1)},bloomW:{value:[${BLOOM_WEIGHTS}]}}))},
|
|
1183
1186
|
{target:"write",material:track(postMaterial(composite,u({bright:{value:brightA.texture},intensity:{value:step.intensity}})))}];
|
|
1184
1187
|
}
|
|
1185
1188
|
case "flare":{
|
|
@@ -1271,6 +1274,7 @@ SlopcameraOverlay.onFrame(({frame:index})=>{
|
|
|
1271
1274
|
renderer.setRenderTarget(null);renderer.clear(true,true,true);
|
|
1272
1275
|
if(hasPost){
|
|
1273
1276
|
postOutputMaterial.uniforms.source.value=runPostChain(frame,index,track);
|
|
1277
|
+
renderer.setRenderTarget(null);
|
|
1274
1278
|
postMesh.material=postOutputMaterial;renderer.render(postScene,outputCamera);
|
|
1275
1279
|
}else renderer.render(outputScene,outputCamera);
|
|
1276
1280
|
}
|
|
@@ -48,6 +48,7 @@ function catalogEntry<Input, Output extends WorkflowOutputValue>(
|
|
|
48
48
|
});
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
/** Public copy cites this count; `scripts/check-copy.ts` fails on drift. */
|
|
51
52
|
export const BUILT_IN_WORKFLOWS: readonly BuiltInWorkflow[] = Object.freeze([
|
|
52
53
|
catalogEntry(directedScene, {
|
|
53
54
|
description: "Render a prepared immutable spatial scene composition with exact frame timing and retained project audio.",
|
package/dist/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
// @bun
|
|
3
|
-
import{SLOPCAMERA_VERSION,VISUAL_STYLE_PROFILES,artifactSummary,checkDiagramFile,checkDrawingFile,createVisualStyleDirection,getVisualStyleProfile,renderDiagramFile,renderDrawingFile,runMcpServer,starterDrawingSource}from"./index-
|
|
3
|
+
import{SLOPCAMERA_VERSION,VISUAL_STYLE_PROFILES,artifactSummary,checkDiagramFile,checkDrawingFile,createVisualStyleDirection,getVisualStyleProfile,renderDiagramFile,renderDrawingFile,runMcpServer,starterDrawingSource}from"./index-qxfexvj3.js";import{installSkill,pathExists}from"./index-grk47tgs.js";import"./index-z1kd1x8j.js";import"./index-3cxf2c58.js";import"./index-4z9y2wa2.js";import{executeSlopcameraOperation,generateSlopcameraIcon,generateSlopcameraImageGallery,isSlopcameraOperationCode,parseSlopcameraGalleryVary,searchSlopcameraOperations,slopcameraGalleryKinds,slopcameraGalleryLimits,slopcameraIconMaximumRounds,slopcameraOperationCodes,withSlopcameraOperationHostAdmission}from"./index-fevf4ppk.js";import{vectorizeImage}from"./index-mnp0bftq.js";import{generateSlopcameraImageFile,slopcameraGatewayCredentialStatus,slopcameraImageModels}from"./index-nxtmy18t.js";import"./index-b3geqczn.js";import{__require}from"./index-80a36bc0.js";import{readFile,writeFile}from"fs/promises";import{resolve}from"path";function reportUsefulResult(observer){try{if(observer!==undefined)Promise.resolve(observer()).catch(()=>{return})}catch{}}import{maybeShowSupportInvitation,runSupportCommand,supportAdvancedHelp,supportHelpLine}from"@hraness/support-foundation/node";var slopcameraSupportHelpLine=()=>supportHelpLine({command:["slopcamera"]});var slopcameraSupportAdvancedHelp=()=>supportAdvancedHelp({command:["slopcamera"]});var supportProfile={id:"slopcamera",name:"Slopcamera",valueProposition:"Support ongoing development of local media tools for agents.",updates:false};function standaloneSupportEnvironment(){const env={...process.env};process.env.HRANESS_SUPPORT_AUDIENCE="off";process.env.HRANESS_SUPPORT_EMAIL="off";return env}async function runProductSupportCommand(args,options={}){const result=await runSupportCommand(supportProfile,args,{command:["slopcamera"],...options,gitEmail:false});if(result.stdout!=="")process.stdout.write(result.stdout);if(result.stderr!=="")process.stderr.write(result.stderr);return result.exitCode}async function showProductSupportInvitation(options={}){try{await maybeShowSupportInvitation(supportProfile,{usefulResult:true,command:["slopcamera"],...options,gitEmail:false})}catch{}}var slopcameraCliVersion=SLOPCAMERA_VERSION;var help=()=>`slopcamera ${slopcameraCliVersion}
|
|
4
4
|
|
|
5
5
|
Turn source material into deterministic diagrams, images, and canvas assets.
|
|
6
6
|
|
|
@@ -25,7 +25,6 @@ Usage:
|
|
|
25
25
|
slopcamera code execute <operation> --input <JSON>
|
|
26
26
|
slopcamera mcp --root <workspace>
|
|
27
27
|
slopcamera doctor
|
|
28
|
-
slopcamera support [--json|protocol --json|offer --json|shown <id>|release <id>|dismiss|snooze|enable|status --json]
|
|
29
28
|
slopcamera skill path
|
|
30
29
|
slopcamera skill install [--target codex|claude|agents] [--scope user|project] [--force]
|
|
31
30
|
|
|
@@ -65,9 +64,9 @@ into one labelled contact sheet plus a receipt. Use it to review texture,
|
|
|
65
64
|
skybox, backdrop, sprite, or design alternatives, then promote a chosen
|
|
66
65
|
candidate file explicitly \u2014 nothing is applied automatically.
|
|
67
66
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
67
|
+
${slopcameraSupportHelpLine()}
|
|
68
|
+
|
|
69
|
+
Advanced verbs, including the support protocol, live under \`slopcamera help advanced\`.
|
|
71
70
|
|
|
72
71
|
Code mode searches and executes a fixed semantic registry. Execute accepts
|
|
73
72
|
typed JSON for one exact owned operation code; it never evaluates source text.
|
|
@@ -80,12 +79,12 @@ stdout.
|
|
|
80
79
|
`,{flag:"wx"})}catch(error){if(error instanceof Error&&"code"in error&&error.code==="EEXIST"){throw new Error(`Refusing to overwrite existing file: ${filePath}`)}throw error}log(json?JSON.stringify({command:"diagram.sheets.init",source:filePath}):`Created ${filePath}`);reportUsefulResult(dependencies.onUsefulResult);return}if(command==="check"){const checks=await withSlopcameraOperationHostAdmission("slopcamera.diagram.check",async()=>await checkDrawingFile({filePath}),hostAdmissionOptions(dependencies));log(json?JSON.stringify({command:"diagram.sheets.check",source:filePath,...checks}):`Valid drawing: ${checks.sheetCount} sheet${checks.sheetCount===1?"":"s"} (${checks.paper}, ${checks.profile}).`);return}const result=await withSlopcameraOperationHostAdmission("slopcamera.diagram.render",async()=>await renderDrawingFile({filePath,...outDirectory===undefined?{}:{outDirectory}}),hostAdmissionOptions(dependencies));log(json?JSON.stringify({command:"diagram.sheets.render",artifacts:result.artifacts,checks:result.checks}):[result.artifacts.pdf,...result.artifacts.sheets,result.artifacts.receipt].join(`
|
|
81
80
|
`));reportUsefulResult(dependencies.onUsefulResult)}function canonicalArguments(args){const[surface,subcommand,...rest]=args;if(surface==="diagram"){if(subcommand==="init"||subcommand==="check"||subcommand==="render"){return[subcommand,...rest]}throw new Error("Use slopcamera diagram init, check, render, or sheets")}if(surface==="image"){if(subcommand==="vectorize"||subcommand==="generate"||subcommand==="icon"||subcommand==="gallery"){return[subcommand,...rest]}throw new Error("Use slopcamera image vectorize, generate, icon, or gallery")}if(surface==="init"||surface==="check"||surface==="render"||surface==="vectorize"||surface==="generate"){throw new Error(`The flat \`${surface}\` command moved to a namespaced Slopcamera surface.
|
|
82
81
|
|
|
83
|
-
${help}`)}return args}async function main(args,dependencies={}){if(args[0]==="style"){const[action,...words]=args.slice(1);const json=words.at(-1)==="--json";const positionals=json?words.slice(0,-1):words;const log=dependencies.log??console.log;if(action==="list"&&positionals.length===0){log(json?JSON.stringify({schemaVersion:1,styles:VISUAL_STYLE_PROFILES}):["Visual style foundations (authoring guidance; no effects are applied):",...VISUAL_STYLE_PROFILES.map((style)=>`${style.id} ${style.name}
|
|
82
|
+
${help()}`)}return args}async function main(args,dependencies={}){if(args[0]==="style"){const[action,...words]=args.slice(1);const json=words.at(-1)==="--json";const positionals=json?words.slice(0,-1):words;const log=dependencies.log??console.log;if(action==="list"&&positionals.length===0){log(json?JSON.stringify({schemaVersion:1,styles:VISUAL_STYLE_PROFILES}):["Visual style foundations (authoring guidance; no effects are applied):",...VISUAL_STYLE_PROFILES.map((style)=>`${style.id} ${style.name}
|
|
84
83
|
${style.summary}`),"Use slopcamera style show <id> for palette, cadence, materials, delivery, and review criteria."].join(`
|
|
85
|
-
`));return}if(action==="show"&&positionals.length===1&&!positionals[0].startsWith("-")){let style;try{style=getVisualStyleProfile(positionals[0])}catch{throw new Error(`Unknown visual style: ${JSON.stringify(positionals[0].slice(0,128))}. Use slopcamera style list.`)}log(json?JSON.stringify({schemaVersion:1,style}):createVisualStyleDirection(style.id));return}throw new Error("Use slopcamera style list [--json] or slopcamera style show <id> [--json].")}if(args[0]==="diagram"&&args[1]==="sheets"){await runDrawingSheets(args.slice(2),dependencies);return}if(args[0]==="support"){process.exitCode=await runProductSupportCommand(args.slice(1),dependencies.supportEnvironment===undefined?{}:{env:dependencies.supportEnvironment});return}const[command,...rest]=canonicalArguments(args);if(command===undefined||command==="help"||command==="
|
|
84
|
+
`));return}if(action==="show"&&positionals.length===1&&!positionals[0].startsWith("-")){let style;try{style=getVisualStyleProfile(positionals[0])}catch{throw new Error(`Unknown visual style: ${JSON.stringify(positionals[0].slice(0,128))}. Use slopcamera style list.`)}log(json?JSON.stringify({schemaVersion:1,style}):createVisualStyleDirection(style.id));return}throw new Error("Use slopcamera style list [--json] or slopcamera style show <id> [--json].")}if(args[0]==="diagram"&&args[1]==="sheets"){await runDrawingSheets(args.slice(2),dependencies);return}if(args[0]==="support"){process.exitCode=await runProductSupportCommand(args.slice(1),dependencies.supportEnvironment===undefined?{}:{env:dependencies.supportEnvironment});return}const[command,...rest]=canonicalArguments(args);if(command===undefined||command==="--help"||command==="-h"){console.log(help());return}if(command==="help"){console.log(rest[0]==="advanced"?slopcameraSupportAdvancedHelp():help());return}if(command==="version"||command==="--version"||command==="-v"){console.log(slopcameraCliVersion);return}if(command==="init"){const parsed=parseArguments(rest,new Set);const filePath=resolve(parsed.positionals[0]??"diagram.diagram.json");if(await pathExists(filePath))throw new Error(`Refusing to overwrite existing file: ${filePath}`);await writeFile(filePath,`${JSON.stringify(starter,null,2)}
|
|
86
85
|
`);console.log(`Created ${filePath}`);reportUsefulResult(dependencies.onUsefulResult);return}if(command==="check"){const parsed=parseArguments(rest,new Set(["config"]));const result=await withSlopcameraOperationHostAdmission("slopcamera.diagram.check",async()=>await checkDiagramFile({filePath:requiredPositional(parsed,0,"diagram file"),...parsed.options.config===undefined?{}:{configPath:parsed.options.config}}),hostAdmissionOptions(dependencies));console.log(`Valid diagram${result.configPath===null?"":` with ${result.configPath}`}.`);printFindings(result.findings);if(parsed.flags.has("strict")&&result.findings.length>0)process.exitCode=2;return}if(command==="render"){const parsed=parseArguments(rest,new Set(["out-dir","config","scale"]));const scale=parsed.options.scale===undefined?undefined:Number.parseFloat(parsed.options.scale);const result=await withSlopcameraOperationHostAdmission("slopcamera.diagram.render",async()=>await renderDiagramFile({filePath:requiredPositional(parsed,0,"diagram file"),...parsed.options["out-dir"]===undefined?{}:{outDirectory:parsed.options["out-dir"]},...parsed.options.config===undefined?{}:{configPath:parsed.options.config},...scale===undefined?{}:{scale}}),hostAdmissionOptions(dependencies));console.log(artifactSummary(result.artifacts));printFindings(result.findings);reportUsefulResult(dependencies.onUsefulResult);return}if(command==="vectorize"){const parsed=parseArguments(rest,new Set(["output","duotone","alpha-cutoff","timeout-ms"]));const unknownFlags=[...parsed.flags].filter((flag)=>flag!=="json");if(unknownFlags.length>0){throw new Error(`Unknown vectorize option: --${unknownFlags[0]}`)}if(parsed.positionals.length>1){throw new Error("slopcamera image vectorize accepts exactly one raster input")}const output=requiredOption(parsed,"output");if(!output.toLowerCase().endsWith(".svg")){throw new Error("--output must end in .svg")}const alphaCutoff=parsePositiveInteger(parsed.options["alpha-cutoff"],"alpha-cutoff");const timeoutMs=parsePositiveInteger(parsed.options["timeout-ms"],"timeout-ms");const duotone=parseDuotone(parsed.options.duotone);const result=await withSlopcameraOperationHostAdmission("slopcamera.image.vectorize",async(lease)=>await(dependencies.vectorize??vectorizeImage)(requiredPositional(parsed,0,"raster image"),{...alphaCutoff===undefined?{}:{alphaCutoff},...duotone===undefined?{}:{duotone},...timeoutMs===undefined?{}:{limits:{maxDurationMs:timeoutMs}},inheritedFileDescriptors:[lease.inheritedFileDescriptor],outputPath:output}),hostAdmissionOptions(dependencies));if(parsed.flags.has("json")){(dependencies.log??console.log)(JSON.stringify({...result.receipt,outputPath:result.outputPath},null,2))}else{(dependencies.log??console.log)(`Vectorized ${result.receipt.width}\xD7${result.receipt.height} with `+`${result.receipt.profile}/${result.receipt.representation}: ${result.outputPath}`)}reportUsefulResult(dependencies.onUsefulResult);return}if(command==="generate"){const parsed=parseArguments(rest,new Set(["model","output"]));const unknownFlags=[...parsed.flags].filter((flag)=>flag!=="json");if(unknownFlags.length>0){throw new Error(`Unknown generate option: --${unknownFlags[0]}`)}if(parsed.positionals.length!==1){throw new Error("slopcamera image generate accepts exactly one prompt")}const model=parsed.options.model??slopcameraImageModels[1];if(model.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(model)){throw new Error("--model must be a bounded Vercel AI Gateway provider/model id")}const result=await withSlopcameraOperationHostAdmission("slopcamera.image.generate",async()=>await(dependencies.generate??generateSlopcameraImageFile)({model,prompt:requiredPositional(parsed,0,"prompt"),outputPath:requiredOption(parsed,"output")}),hostAdmissionOptions(dependencies));if(parsed.flags.has("json")){(dependencies.log??console.log)(JSON.stringify(result,null,2))}else{(dependencies.log??console.log)(`Generated ${result.mediaType} with ${result.model}: ${result.outputPath} (${result.bytes} bytes, request ${result.requestId})`)}reportUsefulResult(dependencies.onUsefulResult);return}if(command==="icon"){const parsed=parseArguments(rest,new Set(["model","output","ink","purpose","rounds","critique-model"]));const unknownFlags=[...parsed.flags].filter((flag)=>flag!=="json"&&flag!=="keep-raster");if(unknownFlags.length>0){throw new Error(`Unknown icon option: --${unknownFlags[0]}`)}if(parsed.positionals.length!==1){throw new Error("slopcamera image icon accepts exactly one subject")}const output=requiredOption(parsed,"output");if(!output.toLowerCase().endsWith(".svg")){throw new Error("--output must end in .svg")}const model=parsed.options.model??slopcameraImageModels[1];if(model.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(model)){throw new Error("--model must be a bounded Vercel AI Gateway provider/model id")}const critiqueModel=parsed.options["critique-model"];if(critiqueModel!==undefined&&(critiqueModel.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(critiqueModel))){throw new Error("--critique-model must be a bounded Vercel AI Gateway provider/model id")}const ink=parsed.options.ink;if(ink!==undefined&&!/^#[a-f0-9]{3}(?:[a-f0-9]{3})?$/iu.test(ink)){throw new Error("--ink must be a #rgb or #rrggbb color")}const purpose=parsed.options.purpose;if(purpose!==undefined&&purpose!=="mark"&&purpose!=="illustration"){throw new Error("--purpose must be mark or illustration")}const rounds=parsePositiveInteger(parsed.options.rounds,"rounds");if(rounds!==undefined&&rounds>slopcameraIconMaximumRounds){throw new Error(`--rounds must be at most ${slopcameraIconMaximumRounds}`)}const result=await withSlopcameraOperationHostAdmission("slopcamera.image.icon",async(lease)=>await(dependencies.icon??generateSlopcameraIcon)({keepRaster:parsed.flags.has("keep-raster"),model,outputPath:output,subject:requiredPositional(parsed,0,"subject"),inheritedFileDescriptors:[lease.inheritedFileDescriptor],...critiqueModel===undefined?{}:{critiqueModel},...ink===undefined?{}:{ink},...purpose===undefined?{}:{purpose},...rounds===undefined?{}:{rounds}}),hostAdmissionOptions(dependencies));if(parsed.flags.has("json")){(dependencies.log??console.log)(JSON.stringify(result,null,2))}else{const scored=result.attempts.map((attempt)=>attempt.score===undefined?`#${attempt.round}`:`#${attempt.round}=${attempt.score}`).join(" ");(dependencies.log??console.log)(`Icon ${result.svgSha256.slice(0,12)} round ${result.selectedRound}/${result.attempts.length} (${scored}): ${result.outputPath}`)}return}if(command==="gallery"){const parsed=parseArguments(rest,new Set(["model","output-dir","kind","count","vary","candidates","cell","timeout-ms"]));const unknownFlags=[...parsed.flags].filter((flag)=>flag!=="json"&&flag!=="tile"&&flag!=="no-tile");if(unknownFlags.length>0){throw new Error(`Unknown gallery option: --${unknownFlags[0]}`)}if(parsed.flags.has("tile")&&parsed.flags.has("no-tile")){throw new Error("--tile and --no-tile are mutually exclusive")}if(parsed.positionals.length!==1){throw new Error("slopcamera image gallery accepts exactly one subject")}const model=parsed.options.model??slopcameraImageModels[1];if(model.length>256||!/^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._:-]*$/iu.test(model)){throw new Error("--model must be a bounded Vercel AI Gateway provider/model id")}const kind=parsed.options.kind;if(kind!==undefined&&!slopcameraGalleryKinds.includes(kind)){throw new Error(`--kind must be one of: ${slopcameraGalleryKinds.join(", ")}`)}const count=parsePositiveInteger(parsed.options.count,"count");if(count!==undefined&&count>slopcameraGalleryLimits.candidates){throw new Error(`--count must be at most ${slopcameraGalleryLimits.candidates}`)}const cell=parsePositiveInteger(parsed.options.cell,"cell");if(cell!==undefined&&(cell<slopcameraGalleryLimits.cellEdgeMin||cell>slopcameraGalleryLimits.cellEdgeMax)){throw new Error(`--cell must be between ${slopcameraGalleryLimits.cellEdgeMin} and ${slopcameraGalleryLimits.cellEdgeMax}`)}const timeoutMs=parsePositiveInteger(parsed.options["timeout-ms"],"timeout-ms");const vary=parseGalleryVary(parsed.options.vary);const candidates=parsed.options.candidates===undefined?undefined:await readGalleryCandidates(parsed.options.candidates);if(candidates!==undefined&&!Array.isArray(candidates)){throw new Error("--candidates JSON must be an array of {id, prompt|variant} entries")}const result=await withSlopcameraOperationHostAdmission("slopcamera.image.gallery",async()=>await(dependencies.gallery??generateSlopcameraImageGallery)({subject:requiredPositional(parsed,0,"subject"),outputDir:requiredOption(parsed,"output-dir"),model,...kind===undefined?{}:{kind},...count===undefined?{}:{count},...vary===undefined?{}:{vary},...candidates===undefined?{}:{candidates},...cell===undefined?{}:{cellEdge:cell},...timeoutMs===undefined?{}:{timeoutMs},...parsed.flags.has("tile")?{tiled:true}:parsed.flags.has("no-tile")?{tiled:false}:{}}),hostAdmissionOptions(dependencies));if(parsed.flags.has("json")){(dependencies.log??console.log)(JSON.stringify(result,null,2))}else{const rows=result.candidates.map((candidate)=>candidate.status==="generated"?`#${candidate.index} ${candidate.id} ${candidate.path}`:`#${candidate.index} ${candidate.id} failed`);(dependencies.log??console.log)(`Gallery ${result.counts.generated}/${result.counts.requested} generated (${result.model}): ${result.gallery.path}
|
|
87
86
|
`+`${rows.join(`
|
|
88
87
|
`)}
|
|
89
88
|
receipt ${result.receiptPath}`)}reportUsefulResult(dependencies.onUsefulResult);return}if(command==="code"){const[subcommand,...subcommandArgs]=rest;if(subcommand==="search"){const parsed=parseArguments(subcommandArgs,new Set(["limit"]));if(parsed.flags.size>0||parsed.positionals.length>1){throw new Error("Use slopcamera code search [query] [--limit <number>]")}const limit=parsePositiveInteger(parsed.options.limit,"limit")??slopcameraOperationCodes.length;const operations=searchSlopcameraOperations(parsed.positionals[0]??"",limit);console.log(JSON.stringify({operations},null,2));return}if(subcommand==="execute"){const parsed=parseArguments(subcommandArgs,new Set(["input"]));if(parsed.flags.size>0||parsed.positionals.length!==1){throw new Error("Use slopcamera code execute <operation> --input <JSON>")}const requestedOperation=parsed.positionals[0];const operation=isSlopcameraOperationCode(requestedOperation)?requestedOperation:undefined;if(operation===undefined){throw new Error(`Unknown Slopcamera operation code: ${requestedOperation}`)}const inputText=requiredOption(parsed,"input");if(Buffer.byteLength(inputText,"utf8")>64*1024){throw new Error("--input JSON must be no more than 65536 UTF-8 bytes")}let input;try{input=JSON.parse(inputText)}catch{throw new Error("--input must be valid JSON")}const result=await executeSlopcameraOperation(operation,input,{...hostAdmissionOptions(dependencies)});(dependencies.log??console.log)(JSON.stringify({operation,result},null,2));if(operation==="slopcamera.diagram.render"||operation==="slopcamera.image.vectorize"||operation==="slopcamera.image.generate"){reportUsefulResult(dependencies.onUsefulResult)}return}throw new Error("Use slopcamera code search [query] or slopcamera code execute <operation> --input <JSON>")}if(command==="mcp"){const parsed=parseArguments(rest,new Set(["root"]));if(parsed.positionals.length>0||parsed.flags.size>0){throw new Error("slopcamera mcp accepts only --root <workspace>")}await runMcpServer({rootDirectory:requiredOption(parsed,"root"),serverVersion:slopcameraCliVersion});return}if(command==="doctor"){console.log(`slopcamera ${slopcameraCliVersion}`);console.log(`Bun ${process.versions.bun??"not detected"}`);console.log("Headless diagram SVG/PNG/tldraw renderer ready");console.log(process.platform==="win32"?"Local raster-to-SVG vectorizer unavailable on Windows (fails closed with tool_platform)":"Local raster-to-SVG vectorizer ready without authentication (VTracer downloads on first use)");console.log("Root-relative MCP check/render/scene server ready (trusted local workspace)");const gateway=slopcameraGatewayCredentialStatus();console.log(gateway.available?`Vercel AI Gateway ready via ${gateway.source}`:"Vercel AI Gateway requires AI_GATEWAY_API_KEY or VERCEL_OIDC_TOKEN");return}if(command==="skill"){const[subcommand,...subcommandArgs]=rest;if(subcommand==="path"){const{bundledSkillPath}=await import("./skill-install-gx7afxsd.js");console.log(bundledSkillPath());return}if(subcommand==="install"){const parsed=parseArguments(subcommandArgs,new Set(["target","scope","project"]));const target=parsed.options.target??"codex";const scope=parsed.options.scope??"user";if(!["codex","claude","agents"].includes(target)){throw new Error("--target must be codex, claude, or agents")}if(!["user","project"].includes(scope)){throw new Error("--scope must be user or project")}const destination=await installSkill({target,scope,...parsed.options.project===undefined?{}:{projectDirectory:parsed.options.project},force:parsed.flags.has("force")});console.log(`Installed slopcamera skill at ${destination}`);return}throw new Error("Use slopcamera skill path or install")}throw new Error(`Unknown command: ${command}
|
|
90
89
|
|
|
91
|
-
${help}`)}if(import.meta.main){try{const env=standaloneSupportEnvironment();let usefulResult=false;await main(process.argv.slice(2),{supportEnvironment:env,onUsefulResult:()=>{usefulResult=true}});if(usefulResult&&(process.exitCode??0)===0)await showProductSupportInvitation({env})}catch(error){console.error(error instanceof Error?error.message:String(error));process.exitCode=1}}export{slopcameraCliVersion,main};
|
|
90
|
+
${help()}`)}if(import.meta.main){try{const env=standaloneSupportEnvironment();let usefulResult=false;await main(process.argv.slice(2),{supportEnvironment:env,onUsefulResult:()=>{usefulResult=true}});if(usefulResult&&(process.exitCode??0)===0)await showProductSupportInvitation({env})}catch(error){console.error(error instanceof Error?error.message:String(error));process.exitCode=1}}export{slopcameraCliVersion,main};
|
package/dist/generate.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{createFixedGatewayFetch,generateSlopcameraImage,generateSlopcameraImageFile,resolveSlopcameraGatewayCredential,slopcameraGatewayApiBaseUrl,slopcameraGatewayCredentialStatus,slopcameraImageModels,slopcameraMaximumPromptBytes,slopcameraMaximumRawImageBytes,slopcameraResponseMediaTypes}from"./index-
|
|
2
|
+
import{createFixedGatewayFetch,generateSlopcameraImage,generateSlopcameraImageFile,resolveSlopcameraGatewayCredential,slopcameraGatewayApiBaseUrl,slopcameraGatewayCredentialStatus,slopcameraImageModels,slopcameraMaximumPromptBytes,slopcameraMaximumRawImageBytes,slopcameraResponseMediaTypes}from"./index-nxtmy18t.js";import"./index-80a36bc0.js";export{slopcameraResponseMediaTypes,slopcameraMaximumRawImageBytes,slopcameraMaximumPromptBytes,slopcameraImageModels,slopcameraGatewayCredentialStatus,slopcameraGatewayApiBaseUrl,resolveSlopcameraGatewayCredential,generateSlopcameraImageFile,generateSlopcameraImage,createFixedGatewayFetch};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{executeSlopcameraOperationWithLease,isSlopcameraOperationCode,parseSlopcameraOperationInput,slopcameraOperationHostResourceClaims}from"./index-
|
|
2
|
+
import{executeSlopcameraOperationWithLease,isSlopcameraOperationCode,parseSlopcameraOperationInput,slopcameraOperationHostResourceClaims}from"./index-fevf4ppk.js";import{createDefaultHostResourceCoordinator}from"./index-b3geqczn.js";var workflowIdPattern=/^[a-z0-9]+(?:[._-][a-z0-9]+)*$/u;var workflowStepIdPattern=/^[A-Za-z0-9]+(?:[._:-][A-Za-z0-9]+)*$/u;var defaultMaximumSteps=64;var hardMaximumSteps=256;class SlopcameraWorkflowError extends Error{code;completedSteps;failedStep;constructor(code,message,options={}){super(`[${code}] ${message}`,{cause:options.cause});this.name="SlopcameraWorkflowError";this.code=code;this.completedSteps=Object.freeze([...options.completedSteps??[]].sort((left,right)=>left.index-right.index));if(options.failedStep!==undefined){this.failedStep=Object.freeze({...options.failedStep})}}}function workflowError(code,message){throw new SlopcameraWorkflowError(code,message)}function validateWorkflowId(id){if(typeof id!=="string"||id.length<1||id.length>80||!workflowIdPattern.test(id)){workflowError("INVALID_WORKFLOW","Workflow id must be 1 through 80 lowercase letters, numbers, dots, underscores, or hyphens.")}}function validateStepId(id,completedSteps=[]){if(typeof id!=="string"||id.length<1||id.length>80||!workflowStepIdPattern.test(id)){throw new SlopcameraWorkflowError("INVALID_WORKFLOW_STEP","Step id must be 1 through 80 letters, numbers, dots, underscores, colons, or hyphens.",{completedSteps})}}function defineSlopcameraWorkflow(options){if(typeof options!=="object"||options===null){workflowError("INVALID_WORKFLOW","Workflow definition must be an object.")}validateWorkflowId(options.id);if(!Number.isSafeInteger(options.version)||options.version<1){workflowError("INVALID_WORKFLOW","Workflow version must be a positive safe integer.")}if(typeof options.parseInput!=="function"||typeof options.run!=="function"){workflowError("INVALID_WORKFLOW","Workflow definition requires parseInput and run functions.")}return Object.freeze({id:options.id,version:options.version,parseInput:options.parseInput,run:options.run})}function maximumSteps(value){const resolved=value??defaultMaximumSteps;if(!Number.isSafeInteger(resolved)||resolved<1||resolved>hardMaximumSteps){workflowError("INVALID_WORKFLOW",`maximumSteps must be an integer from 1 through ${hardMaximumSteps}.`)}return resolved}function aborted(completedSteps,cause){return new SlopcameraWorkflowError("WORKFLOW_ABORTED","Workflow execution was aborted.",{cause,completedSteps})}async function runSlopcameraWorkflow(definition,value,options={}){const normalized=defineSlopcameraWorkflow(definition);const limit=maximumSteps(options.maximumSteps);const signal=options.signal??options.dependencies?.signal??new AbortController().signal;const invoked=new Set;const completed=[];const dispatched=[];let acceptingOperations=true;let nextIndex=0;if(signal.aborted)throw aborted(completed);let input;try{input=normalized.parseInput(value)}catch(cause){throw new SlopcameraWorkflowError("INVALID_WORKFLOW_INPUT","Workflow input did not satisfy its parser.",{cause})}const executor=options.executor??((code,operationInput,context2)=>executeSlopcameraOperationWithLease(code,operationInput,context2.hostResourceLease,options.dependencies));const hostResourceCoordinator=options.hostResourceCoordinator??options.dependencies?.hostResourceCoordinator??createDefaultHostResourceCoordinator();const waitTimeoutMilliseconds=options.waitTimeoutMilliseconds??options.dependencies?.waitTimeoutMilliseconds;async function dispatchOperation(id,code,operationInput){if(signal.aborted)throw aborted(completed);validateStepId(id,completed);if(!isSlopcameraOperationCode(code)){throw new SlopcameraWorkflowError("INVALID_WORKFLOW_STEP",`Workflow step ${id} names an unknown Slopcamera operation.`,{completedSteps:completed})}if(invoked.has(id)){throw new SlopcameraWorkflowError("INVALID_WORKFLOW_STEP",`Duplicate workflow step id: ${id}.`,{completedSteps:completed})}if(nextIndex>=limit){throw new SlopcameraWorkflowError("INVALID_WORKFLOW_STEP",`Workflow exceeds its ${String(limit)}-step limit.`,{completedSteps:completed})}const index=nextIndex;nextIndex+=1;invoked.add(id);let normalizedInput;try{normalizedInput=parseSlopcameraOperationInput(code,operationInput)}catch(cause){throw new SlopcameraWorkflowError("INVALID_WORKFLOW_STEP",`Workflow step ${id} has invalid input for ${code}.`,{cause,completedSteps:completed})}try{const result=await hostResourceCoordinator.withLease(slopcameraOperationHostResourceClaims(code),async(hostResourceLease)=>await executor(code,normalizedInput,{hostResourceLease,signal,stepId:id}),{signal,...waitTimeoutMilliseconds===undefined?{}:{waitTimeoutMilliseconds}});completed.push(Object.freeze({id,index,operation:code}));if(signal.aborted)throw aborted(completed);return result}catch(cause){if(cause instanceof SlopcameraWorkflowError&&cause.code==="WORKFLOW_ABORTED"){throw cause}if(signal.aborted)throw aborted(completed,cause);throw new SlopcameraWorkflowError("WORKFLOW_STEP_FAILED",`Workflow step ${id} (${code}) failed.`,{cause,completedSteps:completed,failedStep:{id,operation:code}})}}const context=Object.freeze({signal,operation(id,code,operationInput){if(!acceptingOperations){const closed=Promise.reject(new SlopcameraWorkflowError("INVALID_WORKFLOW_STEP","Workflow operations cannot start after authored workflow code has settled.",{completedSteps:completed}));closed.catch(()=>{return});return closed}const operation=dispatchOperation(id,code,operationInput);dispatched.push(operation);operation.catch(()=>{return});return operation}});let output;let runFailed=false;let runFailure;try{output=await normalized.run(context,input)}catch(cause){runFailed=true;runFailure=cause}finally{acceptingOperations=false}const operationResults=await Promise.allSettled(dispatched);if(signal.aborted)throw aborted(completed,runFailed?runFailure:undefined);if(runFailed){if(runFailure instanceof SlopcameraWorkflowError)throw runFailure;throw new SlopcameraWorkflowError("WORKFLOW_FAILED",`Workflow ${normalized.id} failed in authored code.`,{cause:runFailure,completedSteps:completed})}const operationFailure=operationResults.find((result)=>result.status==="rejected");if(operationFailure!==undefined){const cause=operationFailure.reason;if(cause instanceof SlopcameraWorkflowError)throw cause;throw new SlopcameraWorkflowError("WORKFLOW_STEP_FAILED","A dispatched workflow operation failed.",{cause,completedSteps:completed})}return Object.freeze({workflow:Object.freeze({id:normalized.id,version:normalized.version}),output,steps:Object.freeze([...completed].sort((left,right)=>left.index-right.index))})}
|
|
3
3
|
export{SlopcameraWorkflowError,defineSlopcameraWorkflow,runSlopcameraWorkflow};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
import{VectorizeDeadline,encodeTracePng,loadRaster,normalizedHexColor,parseHexColor,resolveVectorizeLimits,vectorizeHardLimits,vectorizeImage}from"./index-
|
|
2
|
+
import{VectorizeDeadline,encodeTracePng,loadRaster,normalizedHexColor,parseHexColor,resolveVectorizeLimits,vectorizeHardLimits,vectorizeImage}from"./index-mnp0bftq.js";import{SlopcameraCloudError,createFixedGatewayFetch,generateSlopcameraImage,generateSlopcameraImageFile,resolveSlopcameraGatewayCredential,slopcameraGatewayApiBaseUrl,slopcameraImageModels,slopcameraMaximumPromptBytes}from"./index-nxtmy18t.js";import{createDefaultHostResourceCoordinator}from"./index-b3geqczn.js";import{__require}from"./index-80a36bc0.js";import{randomUUID as randomUUID3}from"crypto";import{mkdir as mkdir3,readFile as readFile2,rename as rename2,rm as rm3,writeFile as writeFile3}from"fs/promises";import{dirname as dirname3,join as join2,resolve as resolve3}from"path";var shared='fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"';var builtInIcons=Object.freeze({brain:{viewBox:"0 0 24 24",body:`<path ${shared} d="M9.5 4.5A3.5 3.5 0 0 0 6 8v.35A3.5 3.5 0 0 0 5.5 15 3.5 3.5 0 0 0 9 19.5M14.5 4.5A3.5 3.5 0 0 1 18 8v.35a3.5 3.5 0 0 1 .5 6.65 3.5 3.5 0 0 1-3.5 4.5M9.5 4.5v15M14.5 4.5v15M6 9.5h3.5M14.5 14.5H18"/>`},check:{viewBox:"0 0 24 24",body:`<path ${shared} d="m5 12 4.25 4.25L19 6.5"/>`},code:{viewBox:"0 0 24 24",body:`<path ${shared} d="m8.5 7-5 5 5 5M15.5 7l5 5-5 5M13.5 4l-3 16"/>`},database:{viewBox:"0 0 24 24",body:`<ellipse ${shared} cx="12" cy="5" rx="7.5" ry="3"/><path ${shared} d="M4.5 5v7c0 1.66 3.36 3 7.5 3s7.5-1.34 7.5-3V5M4.5 12v7c0 1.66 3.36 3 7.5 3s7.5-1.34 7.5-3v-7"/>`},document:{viewBox:"0 0 24 24",body:`<path ${shared} d="M6 2.75h8l4 4V21.25H6zM14 2.75v4h4M9 11h6M9 15h6"/>`},globe:{viewBox:"0 0 24 24",body:`<circle ${shared} cx="12" cy="12" r="9"/><path ${shared} d="M3 12h18M12 3c2.4 2.45 3.5 5.45 3.5 9S14.4 18.55 12 21M12 3C9.6 5.45 8.5 8.45 8.5 12s1.1 6.55 3.5 9"/>`},search:{viewBox:"0 0 24 24",body:`<circle ${shared} cx="10.5" cy="10.5" r="6.5"/><path ${shared} d="m15.25 15.25 5 5"/>`},server:{viewBox:"0 0 24 24",body:`<rect ${shared} x="3" y="3.5" width="18" height="7" rx="2"/><rect ${shared} x="3" y="13.5" width="18" height="7" rx="2"/><path ${shared} d="M7 7h.01M7 17h.01M11 7h6M11 17h6"/>`},shield:{viewBox:"0 0 24 24",body:`<path ${shared} d="M12 2.75 20 6v5.5c0 5.25-3.35 8.2-8 10-4.65-1.8-8-4.75-8-10V6z"/>`},user:{viewBox:"0 0 24 24",body:`<circle ${shared} cx="12" cy="8" r="4"/><path ${shared} d="M4.5 21c.5-4.2 3-6.5 7.5-6.5s7 2.3 7.5 6.5"/>`}});function sanitizeIcon(icon){const dangerous=/<\s*(script|style|foreignObject|iframe|object|embed|image|use|a)\b|on[a-z]+\s*=|(?:xlink:)?href\s*=|style\s*=|javascript:|url\s*\(/i;if(dangerous.test(icon.body)){throw new Error("Icon body contains executable or externally embedded SVG content")}if(!/^[-+.\d\s]+$/.test(icon.viewBox.trim())){throw new Error(`Invalid icon viewBox: ${icon.viewBox}`)}return icon}var defaultLabelFontSize=22;var defaultLabelWeight=600;var defaultLineHeightRatio=1.35;var defaultRowGap=8;var iconLabelGap=12;function characterWidthRatio(fontFamily){return fontFamily==="mono"?0.62:0.56}function estimateDiagramTextWidth(text,fontSize,fontFamily="default"){return text.length*fontSize*characterWidthRatio(fontFamily)}function wrapDiagramText(text,maxWidth,fontSize,fontFamily="default"){const explicitLines=text.split(`
|
|
3
3
|
`);const maxCharacters=Math.max(1,Math.floor(maxWidth/(fontSize*characterWidthRatio(fontFamily))));const lines=[];for(const explicitLine of explicitLines){if(explicitLine.length<=maxCharacters){lines.push(explicitLine);continue}const words=explicitLine.split(/\s+/);let current="";for(const word of words){const candidate=current===""?word:`${current} ${word}`;if(candidate.length<=maxCharacters||current===""){current=candidate}else{lines.push(current);current=word}}if(current!=="")lines.push(current)}return lines.length===0?[""]:lines}function resolveRows(shape){if(shape.labelRows!==undefined){return shape.labelRows.map((row)=>({text:row.text,fontSize:row.fontSize??defaultLabelFontSize,fontFamily:row.fontFamily??"default",weight:row.weight??defaultLabelWeight,lineHeight:(row.fontSize??defaultLabelFontSize)*defaultLineHeightRatio}))}if(shape.label===undefined)return[];const fontSize=shape.labelFontSize??defaultLabelFontSize;return[{text:shape.label,fontSize,fontFamily:shape.labelFontFamily??"default",weight:shape.labelWeight??defaultLabelWeight,lineHeight:fontSize*defaultLineHeightRatio}]}function layoutBoxContent(shape,resolvedIconSize){const textWidth=Math.max(1,shape.width-32);const pendingRows=resolveRows(shape).map((row)=>{const lines=wrapDiagramText(row.text,textWidth,row.fontSize,row.fontFamily);const height=lines.length*row.lineHeight;return{...row,lines,height}});const rowGap=shape.labelRows===undefined?0:shape.labelRowGap??defaultRowGap;const rowsHeight=pendingRows.reduce((total,row)=>total+row.height,0)+Math.max(0,pendingRows.length-1)*rowGap;const hasIcon=resolvedIconSize!==undefined;const gap=hasIcon&&pendingRows.length>0?iconLabelGap:0;const contentHeight=(resolvedIconSize??0)+gap+rowsHeight;let cursorY=shape.y+(shape.height-contentHeight)/2;const icon=resolvedIconSize===undefined?undefined:{x:shape.x+(shape.width-resolvedIconSize)/2,y:cursorY,size:resolvedIconSize};if(hasIcon)cursorY+=resolvedIconSize+gap;const rows=pendingRows.map((row)=>{const resolved={...row,y:cursorY};cursorY+=row.height+rowGap;return resolved});return{...icon===undefined?{}:{icon},rows}}import{readFile}from"fs/promises";import{extname,isAbsolute}from"path";import{fileURLToPath}from"url";import{Resvg}from"@resvg/resvg-js";var NebulaSans_Bold_default="./NebulaSans-Bold-26se8aek.otf";var NebulaSans_Bold_default2="./NebulaSans-Bold-bcz7y08t.woff2";var NebulaSans_Book_default="./NebulaSans-Book-8cenzchw.otf";var NebulaSans_Book_default2="./NebulaSans-Book-5ax05zvn.woff2";var tones=["neutral","blue","orange","green","red","purple","yellow"];var defaults={light:{background:"#ffffff",foreground:"#18181b",muted:"#71717a",stroke:"#27272a",tones:{neutral:{fill:"#f4f4f5",stroke:"#52525b",text:"#18181b"},blue:{fill:"#dbeafe",stroke:"#2563eb",text:"#1e3a8a"},orange:{fill:"#ffedd5",stroke:"#ea580c",text:"#7c2d12"},green:{fill:"#dcfce7",stroke:"#16a34a",text:"#14532d"},red:{fill:"#fee2e2",stroke:"#dc2626",text:"#7f1d1d"},purple:{fill:"#f3e8ff",stroke:"#9333ea",text:"#581c87"},yellow:{fill:"#fef9c3",stroke:"#ca8a04",text:"#713f12"}}},dark:{background:"#09090b",foreground:"#fafafa",muted:"#a1a1aa",stroke:"#d4d4d8",tones:{neutral:{fill:"#27272a",stroke:"#a1a1aa",text:"#fafafa"},blue:{fill:"#172554",stroke:"#60a5fa",text:"#dbeafe"},orange:{fill:"#431407",stroke:"#fb923c",text:"#ffedd5"},green:{fill:"#052e16",stroke:"#4ade80",text:"#dcfce7"},red:{fill:"#450a0a",stroke:"#f87171",text:"#fee2e2"},purple:{fill:"#3b0764",stroke:"#c084fc",text:"#f3e8ff"},yellow:{fill:"#422006",stroke:"#facc15",text:"#fef9c3"}}}};function mergeTheme(base,override){if(override===undefined)return base;return{background:override.background??base.background,foreground:override.foreground??base.foreground,muted:override.muted??base.muted,stroke:override.stroke??base.stroke,tones:Object.fromEntries(tones.map((tone)=>[tone,{fill:override.tones?.[tone]?.fill??base.tones[tone].fill,stroke:override.tones?.[tone]?.stroke??base.tones[tone].stroke,text:override.tones?.[tone]?.text??base.tones[tone].text}]))}}function resolveTheme(mode,config){return mergeTheme(defaults[mode],config.theme?.[mode])}var bundledAssetPath=(asset)=>isAbsolute(asset)?asset:fileURLToPath(new URL(asset,import.meta.url));var defaultFont=Object.freeze({family:"Nebula Sans",files:Object.freeze([Object.freeze({embed:true,path:bundledAssetPath(NebulaSans_Book_default2),style:"normal",weight:400}),Object.freeze({embed:true,path:bundledAssetPath(NebulaSans_Bold_default2),style:"normal",weight:700}),Object.freeze({embed:false,path:bundledAssetPath(NebulaSans_Book_default),style:"normal",weight:400}),Object.freeze({embed:false,path:bundledAssetPath(NebulaSans_Bold_default),style:"normal",weight:700})])});var escapeXml=(value)=>value.replaceAll("&","&").replaceAll("<","<").replaceAll(">",">").replaceAll('"',""").replaceAll("'","'");function fontMime(filePath){switch(extname(filePath).toLowerCase()){case".otf":return"font/otf";case".woff":return"font/woff";case".woff2":return"font/woff2";default:return"font/ttf"}}async function fontCss(font){if(font.files===undefined)return"";const faces=await Promise.all(font.files.filter((file)=>file.embed===true).map(async(file)=>{const encoded=(await readFile(file.path)).toString("base64");return`@font-face{font-family:"${escapeXml(font.family)}";src:url(data:${fontMime(file.path)};base64,${encoded});font-weight:${file.weight??400};font-style:${file.style??"normal"};}`}));return faces.join("")}function textSvg(options){const lines=options.lines??wrapDiagramText(options.text,options.width,options.fontSize);const lineHeight=options.lineHeight??options.fontSize*1.25;const anchor=options.align;const x=anchor==="middle"?options.x+options.width/2:anchor==="end"?options.x+options.width:options.x;const dominantBaseline=options.centerLineBoxes?"central":"hanging";return`<text x="${x}" y="${options.y}" text-anchor="${anchor}" dominant-baseline="${dominantBaseline}" fill="${options.color}" opacity="${options.opacity}" font-family="${escapeXml(options.family)}" font-size="${options.fontSize}" font-weight="${options.weight}">${lines.map((line,index)=>options.centerLineBoxes?`<tspan x="${x}" y="${options.y+(index+0.5)*lineHeight}">${escapeXml(line)}</tspan>`:`<tspan x="${x}" dy="${index===0?0:lineHeight}">${escapeXml(line)}</tspan>`).join("")}</text>`}function iconSvg(options){const icon=sanitizeIcon(options.icon);const parts=icon.viewBox.trim().split(/\s+/).map(Number);const width=(parts[2]??24)-(parts[0]??0);const height=(parts[3]??24)-(parts[1]??0);const scale=options.size/Math.max(width,height);return`<g color="${options.color}" opacity="${options.opacity}" transform="translate(${options.x} ${options.y}) scale(${scale}) translate(${-Number(parts[0]??0)} ${-Number(parts[1]??0)})">${icon.body}</g>`}function boxSvg(shape,theme,families,icons){const tone=theme.tones[shape.tone??"neutral"];const strokeWidth=shape.strokeWidth??2;const opacity=shape.opacity??1;const radius=shape.type==="ellipse"?0:Math.min(shape.radius??22,shape.height/2);const geometry=shape.type==="ellipse"?`<ellipse cx="${shape.x+shape.width/2}" cy="${shape.y+shape.height/2}" rx="${shape.width/2}" ry="${shape.height/2}" fill="${shape.fill===false?"none":tone.fill}" stroke="${tone.stroke}" stroke-width="${strokeWidth}"/>`:`<rect x="${shape.x}" y="${shape.y}" width="${shape.width}" height="${shape.height}" rx="${radius}" fill="${shape.fill===false?"none":tone.fill}" stroke="${tone.stroke}" stroke-width="${strokeWidth}"/>`;const icon=shape.icon===undefined?undefined:icons[shape.icon];if(shape.icon!==undefined&&icon===undefined){throw new Error(`Unknown icon "${shape.icon}" on shape ${shape.id}`)}const iconSize=Math.min(shape.iconSize??52,shape.height*0.45,shape.width*0.32);const content=layoutBoxContent(shape,icon===undefined?undefined:iconSize);const iconMarkup=icon===undefined||content.icon===undefined?"":iconSvg({icon,x:content.icon.x,y:content.icon.y,size:content.icon.size,color:tone.text,opacity:1});const labelMarkup=content.rows.map((row)=>textSvg({text:row.text,x:shape.x+16,y:row.y,width:shape.width-32,fontSize:row.fontSize,weight:row.weight,align:"middle",color:tone.text,opacity:1,family:families[row.fontFamily],lineHeight:row.lineHeight,lines:row.lines,centerLineBoxes:true})).join("");return`<g data-shape-id="${escapeXml(shape.id)}" opacity="${opacity}">${geometry}${iconMarkup}${labelMarkup}</g>`}function pointForAnchor(shape,anchor,toward,position){const center={x:shape.x+shape.width/2,y:shape.y+shape.height/2};let resolved=anchor??"auto";if(resolved==="auto"){const dx=toward.x-center.x;const dy=toward.y-center.y;resolved=Math.abs(dx/shape.width)>=Math.abs(dy/shape.height)?dx>=0?"right":"left":dy>=0?"bottom":"top"}const resolvedPosition=position??0.5;if(shape.type==="ellipse"){const projected=(resolvedPosition-0.5)*2;const radial=Math.sqrt(Math.max(0,1-projected*projected))/2;const normalized=resolved==="top"?{x:resolvedPosition,y:0.5-radial}:resolved==="bottom"?{x:resolvedPosition,y:0.5+radial}:resolved==="left"?{x:0.5-radial,y:resolvedPosition}:{x:0.5+radial,y:resolvedPosition};return{x:shape.x+shape.width*normalized.x,y:shape.y+shape.height*normalized.y,normalized}}switch(resolved){case"top":return{x:shape.x+shape.width*resolvedPosition,y:shape.y,normalized:{x:resolvedPosition,y:0}};case"right":return{x:shape.x+shape.width,y:shape.y+shape.height*resolvedPosition,normalized:{x:1,y:resolvedPosition}};case"bottom":return{x:shape.x+shape.width*resolvedPosition,y:shape.y+shape.height,normalized:{x:resolvedPosition,y:1}};case"left":return{x:shape.x,y:shape.y+shape.height*resolvedPosition,normalized:{x:0,y:resolvedPosition}}}}function resolveEdge(spec,edge){const boxes=new Map(spec.shapes.filter((shape)=>shape.type==="rect"||shape.type==="ellipse").map((shape)=>[shape.id,shape]));const from=boxes.get(edge.from);const to=boxes.get(edge.to);if(from===undefined||to===undefined){throw new Error(`Edge ${edge.id} references a missing box`)}const fromCenter={x:from.x+from.width/2,y:from.y+from.height/2};const toCenter={x:to.x+to.width/2,y:to.y+to.height/2};const start=pointForAnchor(from,edge.start,toCenter,edge.startPosition);const end=pointForAnchor(to,edge.end,fromCenter,edge.endPosition);const dx=end.x-start.x;const dy=end.y-start.y;const length=Math.hypot(dx,dy)||1;const bend=edge.bend??0;return{edge,from,to,start,end,control:{x:(start.x+end.x)/2+-dy/length*bend,y:(start.y+end.y)/2+dx/length*bend}}}function resolveEdgeLabel(resolved){const{edge,start,end,control}=resolved;const t=edge.labelPosition??0.5;const oneMinusT=1-t;const point={x:oneMinusT*oneMinusT*start.x+2*oneMinusT*t*control.x+t*t*end.x,y:oneMinusT*oneMinusT*start.y+2*oneMinusT*t*control.y+t*t*end.y};const tangent={x:2*oneMinusT*(control.x-start.x)+2*t*(end.x-control.x),y:2*oneMinusT*(control.y-start.y)+2*t*(end.y-control.y)};const length=Math.hypot(tangent.x,tangent.y)||1;const offset=edge.labelOffset??-14;return{x:point.x+-tangent.y/length*offset,y:point.y+tangent.x/length*offset}}function edgeSvg(resolved,theme,families,strokeWidth){const{edge,start,end,control}=resolved;const toneName=edge.tone??"neutral";const tone=theme.tones[toneName];const marker=edge.arrowhead==="none"?"":edge.arrowhead==="triangle"?`url(#arrow-triangle-${toneName})`:`url(#arrow-open-${toneName})`;const path=`M ${start.x} ${start.y} Q ${control.x} ${control.y} ${end.x} ${end.y}`;const labelPoint=resolveEdgeLabel(resolved);const label=edge.label===undefined?"":`<text x="${labelPoint.x}" y="${labelPoint.y}" text-anchor="middle" dominant-baseline="central" fill="${tone.text}" stroke="${theme.background}" stroke-width="6" paint-order="stroke" font-family="${escapeXml(families[edge.labelFontFamily??"default"])}" font-size="${edge.labelFontSize??18}" font-weight="${edge.labelWeight??600}">${escapeXml(edge.label)}</text>`;return`<g data-edge-id="${escapeXml(edge.id)}"><path d="${path}" fill="none" stroke="${tone.stroke}" stroke-width="${strokeWidth}" stroke-linecap="round" marker-end="${marker}"/>${label}</g>`}function shapeSvg(shape,theme,families,icons){if(shape.type==="rect"||shape.type==="ellipse"){return boxSvg(shape,theme,families,icons)}if(shape.type==="line"){return`<line data-shape-id="${escapeXml(shape.id)}" x1="${shape.x}" y1="${shape.y}" x2="${shape.x2}" y2="${shape.y2}" stroke="${theme.tones[shape.tone??"neutral"].stroke}" stroke-width="${shape.strokeWidth??3}" stroke-linecap="round" opacity="${shape.opacity??1}"/>`}const text=shape;return textSvg({text:text.text,x:text.x,y:text.y,width:text.width??Math.max(text.text.length*(text.fontSize??24)*0.58,12),fontSize:text.fontSize??24,weight:text.weight??500,align:text.align??"start",color:theme.tones[text.tone??"neutral"].text,opacity:text.opacity??1,family:families[text.fontFamily??"default"]})}async function renderSvg(spec,mode,config,options={}){const edgeStrokeWidth=options.edgeStrokeWidth??3;if(!Number.isFinite(edgeStrokeWidth)||edgeStrokeWidth<0.5||edgeStrokeWidth>4)throw new Error("Edge stroke width must be between 0.5 and 4");const theme=resolveTheme(mode,config);const font=config.font??defaultFont;const families={default:font.family,mono:font.monoFamily??"ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace"};const icons=config.icons??{};const embeddedFonts=await fontCss(font);const markerDefinitions=Object.entries(theme.tones).map(([toneName,tone])=>`<marker id="arrow-open-${toneName}" markerWidth="12" markerHeight="12" refX="10" refY="6" orient="auto" markerUnits="strokeWidth"><path d="M2 2 10 6 2 10" fill="none" stroke="${tone.stroke}" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round"/></marker><marker id="arrow-triangle-${toneName}" markerWidth="11" markerHeight="11" refX="9" refY="5.5" orient="auto" markerUnits="strokeWidth"><path d="M1 1 10 5.5 1 10z" fill="${tone.stroke}"/></marker>`).join("");const edgeMarkup=(spec.edges??[]).map((edge)=>edgeSvg(resolveEdge(spec,edge),theme,families,edgeStrokeWidth)).join("");const shapeMarkup=spec.shapes.map((shape)=>shapeSvg(shape,theme,families,icons)).join("");const svg=[`<svg xmlns="http://www.w3.org/2000/svg" width="${spec.canvas.width}" height="${spec.canvas.height}" viewBox="0 0 ${spec.canvas.width} ${spec.canvas.height}" role="img" aria-labelledby="diagram-title" color-scheme="${mode}">`,`<title id="diagram-title">${escapeXml(spec.name)}</title>`,`<style>${embeddedFonts}text{font-kerning:normal;text-rendering:geometricPrecision}</style>`,`<rect width="100%" height="100%" fill="${theme.background}"/>`,`<defs>${markerDefinitions}</defs>`,edgeMarkup,shapeMarkup,"</svg>"].join("");return{mode,svg,width:spec.canvas.width,height:spec.canvas.height}}function renderPng(rendered,config,scale=2){const font=config.font??defaultFont;const fontFiles=font.files?.map((file)=>file.path)??[];const renderer=new Resvg(rendered.svg,{fitTo:{mode:"zoom",value:scale},font:{loadSystemFonts:true,...fontFiles.length===0?{}:{fontFiles},defaultFontFamily:font.family}});return renderer.render().asPng()}function boxOutsideCanvas(shape,spec){return shape.x<0||shape.y<0||shape.x+shape.width>spec.canvas.width||shape.y+shape.height>spec.canvas.height}function lintDiagram(spec){const findings=[];const boxes=spec.shapes.filter((shape)=>shape.type==="rect"||shape.type==="ellipse");for(const shape of boxes){if(boxOutsideCanvas(shape,spec)){findings.push({code:"outside-canvas",message:`${shape.id} extends beyond the canvas`,shapeIds:[shape.id]})}const labels=shape.labelRows?.map((row)=>row.text)??(shape.label===undefined?[]:[shape.label]);const longLabel=labels.find((label)=>label.length>32);if(longLabel!==undefined){findings.push({code:"long-label",message:`${shape.id} has a ${longLabel.length}-character label; prefer a short noun phrase`,shapeIds:[shape.id]})}const iconSize=shape.icon===undefined?undefined:Math.min(shape.iconSize??52,shape.height*0.45,shape.width*0.32);const content=layoutBoxContent(shape,iconSize);const contentTop=content.icon?.y??content.rows[0]?.y;const finalRow=content.rows.at(-1);const contentBottom=finalRow===undefined?content.icon===undefined?undefined:content.icon.y+content.icon.size:finalRow.y+finalRow.height;const contentWidth=Math.max(1,shape.width-32);const exceedsWidth=content.rows.some((row)=>row.lines.some((line)=>estimateDiagramTextWidth(line,row.fontSize,row.fontFamily)>contentWidth));if(contentTop!==undefined&&contentBottom!==undefined&&(contentTop<shape.y||contentBottom>shape.y+shape.height||exceedsWidth)){findings.push({code:"label-overflow",message:`${shape.id} label and icon content exceeds the box bounds; increase the box or reduce authored sizes`,shapeIds:[shape.id]})}if(shape.width<120||shape.height<64){findings.push({code:"small-target",message:`${shape.id} is small enough to make its label or icon hard to scan`,shapeIds:[shape.id]})}}if(boxes.length>9){findings.push({code:"too-many-elements",message:`The diagram has ${boxes.length} primary shapes; consider a higher-level visual`,shapeIds:boxes.map((shape)=>shape.id)})}const sharedTerminals=new Map;for(const edge of spec.edges??[]){const resolved=resolveEdge(spec,edge);for(const terminal of[{kind:"start",point:resolved.start,shapeId:edge.from},{kind:"end",point:resolved.end,shapeId:edge.to}]){const key=`${terminal.kind}:${terminal.shapeId}:${terminal.point.x.toFixed(3)}:${terminal.point.y.toFixed(3)}`;const group=sharedTerminals.get(key)??[];group.push({edgeId:edge.id,shapeId:terminal.shapeId});sharedTerminals.set(key,group)}const length=Math.hypot(resolved.end.x-resolved.start.x,resolved.end.y-resolved.start.y);if(length<96){findings.push({code:"short-arrow",message:`${edge.id} is ${Math.round(length)}px long; leave more space between connected shapes`,shapeIds:[edge.from,edge.to]})}if(edge.label!==undefined&&edge.label.length>24){findings.push({code:"long-edge-label",message:`${edge.id} has a long connector label; prefer one short relation`,shapeIds:[edge.from,edge.to]})}}for(const group of sharedTerminals.values()){if(group.length<2)continue;findings.push({code:"shared-edge-port",message:`${group.map(({edgeId})=>edgeId).join(", ")} share one connector port; set distinct startPosition or endPosition values`,shapeIds:[group[0].shapeId]})}return findings}var diagramVersion=1;var stackLayoutDefaults={gap:160,padding:64,align:"center"};var coordinatePrecision=3;var coordinateScale=10**coordinatePrecision;class StackLayoutError extends Error{issues;constructor(issues){super(`Invalid stack layout:
|
|
4
4
|
${issues.map((issue)=>`- ${issue}`).join(`
|
|
5
5
|
`)}`);this.name="StackLayoutError";this.issues=issues}}function coordinateFromUnits(value){return value/coordinateScale}function normalizedScaledCoordinate(value){const scaled=value*coordinateScale;return normalizedGridValue(scaled)}function normalizedGridValue(value){const nearest=Math.round(value);return Math.abs(value-nearest)<=0.000000001?nearest:value}function floorGridValue(value){return Math.floor(normalizedGridValue(value))}function ceilGridValue(value){return Math.ceil(normalizedGridValue(value))}function coordinateUnits(value,label,positive,rounding,issues){if(!Number.isFinite(value)||(positive?value<=0:value<0)){issues.push(positive?`${label} must be positive`:`${label} must not be negative`);return 0}const scaled=normalizedScaledCoordinate(value);const units=rounding==="ceil"?ceilGridValue(scaled):floorGridValue(scaled);if(!Number.isSafeInteger(units)){issues.push(`${label} must be a finite coordinate within the supported range`);return 0}if(positive&&units<1){issues.push(`${label} must occupy at least ${coordinateFromUnits(1)}px in the layout grid`);return 0}return units}function positionedShape(shape,mainPosition,crossPosition,horizontal){return{...shape,x:coordinateFromUnits(horizontal?mainPosition:crossPosition),y:coordinateFromUnits(horizontal?crossPosition:mainPosition)}}function authoredMainSize(shape,horizontal){return horizontal?shape.width:shape.height}function emittedMainPlacement(shapes,horizontal,gap,issues){if(shapes.length===0)return{offsets:[],span:0};const offsets=[];let offset=0;for(const[index,shape]of shapes.entries()){offsets.push(offset);if(index===shapes.length-1)break;const advance=coordinateUnits(authoredMainSize(shape,horizontal)+gap,`space after shape ${shape.id}`,true,"ceil",issues);offset+=advance;if(!Number.isSafeInteger(offset)){issues.push("stack positions exceed the supported coordinate range");return{offsets,span:Number.POSITIVE_INFINITY}}}const last=shapes.at(-1);const span=offset+(last===undefined?0:normalizedScaledCoordinate(authoredMainSize(last,horizontal)));if(!Number.isFinite(span)||!Number.isSafeInteger(Math.ceil(span))){issues.push("stack extent exceeds the supported coordinate range")}return{offsets,span}}function clampedGridPosition(ideal,minimum,maximum){return Math.min(maximum,Math.max(minimum,Math.round(ideal)))}function resolvedAnchors(fromIndex,toIndex,horizontal){const forward=fromIndex<toIndex;if(horizontal){return forward?{start:"right",end:"left"}:{start:"left",end:"right"}}return forward?{start:"bottom",end:"top"}:{start:"top",end:"bottom"}}function resolvedEdge(edge,indexes,horizontal){const fromIndex=indexes.get(edge.from);const toIndex=indexes.get(edge.to);if(fromIndex===undefined||toIndex===undefined){throw new StackLayoutError([`edge ${edge.id} must reference shapes that belong to the stack`])}const anchors=resolvedAnchors(fromIndex,toIndex,horizontal);return{...edge,start:anchors.start,end:anchors.end,bend:0}}function stackStructureIssues(source){const issues=[];if(source.shapes.length<1||source.shapes.length>9){issues.push("stack layouts must contain between 1 and 9 shapes")}const indexes=new Map;const recordIds=new Set;for(const[index,shape]of source.shapes.entries()){if(recordIds.has(shape.id)){issues.push(`shape id ${shape.id} is duplicated`)}else{indexes.set(shape.id,index);recordIds.add(shape.id)}}const connectedPairs=new Set;for(const stackEdge of source.edges??[]){const edge=stackEdge;if(recordIds.has(edge.id)){issues.push(`edge id ${edge.id} is duplicated`)}recordIds.add(edge.id);const fromIndex=indexes.get(edge.from);const toIndex=indexes.get(edge.to);if(fromIndex===undefined){issues.push(`edge ${edge.id} has unknown from id ${edge.from}`)}if(toIndex===undefined){issues.push(`edge ${edge.id} has unknown to id ${edge.to}`)}if(fromIndex!==undefined&&toIndex!==undefined){if(fromIndex===toIndex){issues.push(`edge ${edge.id} cannot connect a shape to itself`)}else{if(Math.abs(fromIndex-toIndex)!==1){issues.push(`edge ${edge.id} must connect adjacent stack shapes`)}const pair=[fromIndex,toIndex].sort((left,right)=>left-right).join(":");if(connectedPairs.has(pair)){issues.push(`edge ${edge.id} duplicates a connection between the same stack shapes`)}connectedPairs.add(pair)}}if(edge.start!==undefined&&edge.start!=="auto"){issues.push(`edge ${edge.id}.start must be auto or omitted in a stack layout`)}if(edge.end!==undefined&&edge.end!=="auto"){issues.push(`edge ${edge.id}.end must be auto or omitted in a stack layout`)}if(edge.bend!==undefined&&edge.bend!==0){issues.push(`edge ${edge.id}.bend must be 0 or omitted in a stack layout`)}}return issues}function resolveStackLayout(source){const issues=[...stackStructureIssues(source)];const horizontal=source.layout.direction==="horizontal";coordinateUnits(source.canvas.width,"canvas width",true,"floor",issues);coordinateUnits(source.canvas.height,"canvas height",true,"floor",issues);const gapValue=source.layout.gap??stackLayoutDefaults.gap;coordinateUnits(gapValue,"stack gap",false,"ceil",issues);const paddingValue=source.canvas.padding??stackLayoutDefaults.padding;const padding=coordinateUnits(paddingValue,"canvas padding",false,"ceil",issues);for(const shape of source.shapes){coordinateUnits(shape.width,`shape ${shape.id} width`,true,"ceil",issues);coordinateUnits(shape.height,`shape ${shape.id} height`,true,"ceil",issues)}const align=source.layout.align??stackLayoutDefaults.align;const canvasMainValue=horizontal?source.canvas.width:source.canvas.height;const canvasCrossValue=horizontal?source.canvas.height:source.canvas.width;const mainPlacement=emittedMainPlacement(source.shapes,horizontal,gapValue,issues);const maximumMainStart=floorGridValue(normalizedScaledCoordinate(canvasMainValue-paddingValue)-mainPlacement.span);if(padding>maximumMainStart){const requiredMain=ceilGridValue(mainPlacement.span);const availableMain=floorGridValue(normalizedScaledCoordinate(canvasMainValue-paddingValue*2));issues.push(`${source.layout.direction} stack needs ${coordinateFromUnits(requiredMain)}px but only ${coordinateFromUnits(availableMain)}px remain inside ${paddingValue}px padding`)}const crossStarts=source.shapes.map((shape)=>{const authoredSize=horizontal?shape.height:shape.width;const maximum=floorGridValue(normalizedScaledCoordinate(canvasCrossValue-paddingValue-authoredSize));if(padding>maximum){const requiredCross=ceilGridValue(normalizedScaledCoordinate(authoredSize));const availableCross=floorGridValue(normalizedScaledCoordinate(canvasCrossValue-paddingValue*2));issues.push(`shape ${shape.id} needs ${coordinateFromUnits(requiredCross)}px on the cross axis but only ${coordinateFromUnits(availableCross)}px remain inside ${paddingValue}px padding`)}return{authoredSize,maximum}});if(issues.length>0)throw new StackLayoutError(issues);const mainStart=clampedGridPosition((normalizedScaledCoordinate(canvasMainValue)-mainPlacement.span)/2,padding,maximumMainStart);const shapes=source.shapes.map((shape,index)=>{const crossStart=crossStarts[index];if(crossStart===undefined){throw new StackLayoutError([`shape ${shape.id} has no cross-axis placement`])}const crossPosition=align==="start"?padding:align==="end"?crossStart.maximum:clampedGridPosition(normalizedScaledCoordinate((canvasCrossValue-crossStart.authoredSize)/2),padding,crossStart.maximum);const mainPosition=mainStart+(mainPlacement.offsets[index]??0);const positioned=positionedShape(shape,mainPosition,crossPosition,horizontal);return positioned});const indexes=new Map(shapes.map((shape,index)=>[shape.id,index]));const edges=source.edges?.map((edge)=>resolvedEdge(edge,indexes,horizontal));return{...source.$schema===undefined?{}:{$schema:source.$schema},version:source.version,name:source.name,canvas:{width:source.canvas.width,height:source.canvas.height,padding:paddingValue},shapes,...edges===undefined?{}:{edges}}}function resolveDiagramSource(source){return"layout"in source?resolveStackLayout(source):source}var tones2=new Set(["neutral","blue","orange","green","red","purple","yellow"]);var anchors=new Set(["auto","top","right","bottom","left"]);var fontFamilies=new Set(["default","mono"]);var stackDirections=new Set(["horizontal","vertical"]);var stackAlignments=new Set(["start","center","end"]);var idPattern=/^[A-Za-z0-9][A-Za-z0-9_-]*$/;var namePattern=/^[a-z0-9]+(?:-[a-z0-9]+)*$/;class DiagramValidationError extends Error{issues;constructor(issues){super(`Invalid diagram specification:
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
var vectorizeProfileNames=["balanced","detailed","photo"];class VectorizeError extends Error{code;details;constructor(code,message,details={},options){super(message,options);this.name="VectorizeError";this.code=code;this.details=details}}var vectorizeHardLimits=Object.freeze({maxDecodedPixels:16777216,maxDimension:4096,maxDurationMs:120000,maxInputBytes:16*1024*1024,maxOutputBytes:2000000,maxPaths:12000});var vectorizeDefaultLimits=Object.freeze({...vectorizeHardLimits,maxDurationMs:30000});var limitNames=Object.keys(vectorizeHardLimits);function resolveVectorizeLimits(input){const resolved={maxDecodedPixels:input?.maxDecodedPixels??vectorizeDefaultLimits.maxDecodedPixels,maxDimension:input?.maxDimension??vectorizeDefaultLimits.maxDimension,maxDurationMs:input?.maxDurationMs??vectorizeDefaultLimits.maxDurationMs,maxInputBytes:input?.maxInputBytes??vectorizeDefaultLimits.maxInputBytes,maxOutputBytes:input?.maxOutputBytes??vectorizeDefaultLimits.maxOutputBytes,maxPaths:input?.maxPaths??vectorizeDefaultLimits.maxPaths};for(const name of limitNames){const value=resolved[name];const hardLimit=vectorizeHardLimits[name];if(!Number.isInteger(value)||value<1||value>hardLimit){throw new VectorizeError("invalid_input",`${name} must be a positive integer no greater than ${hardLimit}.`,{hardLimit,name,value})}}return Object.freeze(resolved)}class VectorizeDeadline{#deadline;constructor(durationMs){this.#deadline=performance.now()+durationMs}assert(stage){if(this.remainingMs()<=0){throw new VectorizeError("timeout",`Vectorization timed out during ${stage}.`,{stage})}}remainingMs(){return Math.max(0,Math.ceil(this.#deadline-performance.now()))}}import{constants}from"fs";import{open,realpath}from"fs/promises";import{resolve}from"path";import sharp from"sharp";import{createHash}from"crypto";function sha256(input){return createHash("sha256").update(input).digest("hex")}function sanitizedTraceRgba(rgba,minimumAlpha){if(rgba.length===0||rgba.length%4!==0){throw new VectorizeError("invalid_input","Trace sanitation requires nonempty RGBA pixels.")}if(!Number.isInteger(minimumAlpha)||minimumAlpha<1||minimumAlpha>64){throw new VectorizeError("invalid_input","alphaCutoff must be an integer from 1 through 64.")}const sanitized=Uint8Array.from(rgba);for(let index=0;index<sanitized.length;index+=4){if(sanitized[index+3]>=minimumAlpha)continue;sanitized[index]=0;sanitized[index+1]=0;sanitized[index+2]=0;sanitized[index+3]=0}return sanitized}function alphaPlaneTraceRgba(rgba){if(rgba.length===0||rgba.length%4!==0){throw new VectorizeError("invalid_input","An alpha trace requires nonempty RGBA pixels.")}const grayscale=new Uint8Array(rgba.length);for(let index=0;index<rgba.length;index+=4){const alpha=rgba[index+3];grayscale[index]=alpha;grayscale[index+1]=alpha;grayscale[index+2]=alpha;grayscale[index+3]=255}return grayscale}function lowAlphaMassRatio(rgba,cutoff){if(rgba.length===0||rgba.length%4!==0){throw new VectorizeError("invalid_input","Low-alpha measurement requires nonempty RGBA pixels.")}if(!Number.isInteger(cutoff)||cutoff<1||cutoff>255){throw new VectorizeError("invalid_input","The low-alpha cutoff must be a byte.")}let lowAlphaMass=0;let visibleAlphaMass=0;for(let index=3;index<rgba.length;index+=4){const alpha=rgba[index];visibleAlphaMass+=alpha;if(alpha>0&&alpha<cutoff)lowAlphaMass+=alpha}return visibleAlphaMass===0?0:lowAlphaMass/visibleAlphaMass}function hasFractionalAlpha(rgba){for(let index=3;index<rgba.length;index+=4){const alpha=rgba[index];if(alpha>0&&alpha<255)return true}return false}function normalizedPremultipliedRmse(source,candidate){assertComparableRgba(source,candidate,"RMSE");let squaredError=0;for(let index=0;index<source.length;index+=4){const sourceAlpha=source[index+3]/255;const candidateAlpha=candidate[index+3]/255;for(let channel=0;channel<3;channel+=1){const difference=source[index+channel]*sourceAlpha-candidate[index+channel]*candidateAlpha;squaredError+=difference*difference}const alphaDifference=source[index+3]-candidate[index+3];squaredError+=alphaDifference*alphaDifference}return Math.sqrt(squaredError/source.length)/255}function normalizedAlphaRmse(source,candidate){assertComparableRgba(source,candidate,"Alpha RMSE");let squaredError=0;for(let index=3;index<source.length;index+=4){const difference=source[index]-candidate[index];squaredError+=difference*difference}return Math.sqrt(squaredError/(source.length/4))/255}function assertComparableRgba(source,candidate,label){if(source.length===0||source.length!==candidate.length||source.length%4!==0){throw new VectorizeError("invalid_input",`${label} inputs must be equally sized nonempty RGBA buffers.`)}}function measureSupport(source,candidate,width,height,dilation=1){if(!Number.isInteger(width)||!Number.isInteger(height)||width<1||height<1||source.length!==width*height*4||candidate.length!==source.length){throw new VectorizeError("invalid_input","Support metrics require equally sized rectangular RGBA buffers.")}if(!Number.isInteger(dilation)||dilation<0||dilation>4){throw new VectorizeError("invalid_input","Support dilation must be an integer from 0 to 4.")}const support=new Uint8Array(width*height);for(let y=0;y<height;y+=1){for(let x=0;x<width;x+=1){let covered=false;for(let dy=-dilation;dy<=dilation&&!covered;dy+=1){for(let dx=-dilation;dx<=dilation;dx+=1){const neighborX=x+dx;const neighborY=y+dy;if(neighborX>=0&&neighborX<width&&neighborY>=0&&neighborY<height&&source[(neighborY*width+neighborX)*4+3]>0){covered=true;break}}}support[y*width+x]=covered?1:0}}let candidateAlphaMass=0;let outsideAlphaMass=0;let recalledAlphaMass=0;let sourceAlphaMass=0;for(let pixel=0;pixel<width*height;pixel+=1){const sourceAlpha=source[pixel*4+3]/255;const candidateAlpha=candidate[pixel*4+3]/255;sourceAlphaMass+=sourceAlpha;candidateAlphaMass+=candidateAlpha;recalledAlphaMass+=Math.min(sourceAlpha,candidateAlpha);if(support[pixel]===0)outsideAlphaMass+=candidateAlpha}if(sourceAlphaMass===0||candidateAlphaMass===0){throw new VectorizeError("quality_limit","Source and candidate must contain visible pixels.")}return{outsideAlphaRatio:outsideAlphaMass/candidateAlphaMass,supportRecall:recalledAlphaMass/sourceAlphaMass}}function parseHexColor(value){const shorthand=value.match(/^#([a-f0-9])([a-f0-9])([a-f0-9])$/iu);if(shorthand!==null){return[Number.parseInt(`${shorthand[1]}${shorthand[1]}`,16),Number.parseInt(`${shorthand[2]}${shorthand[2]}`,16),Number.parseInt(`${shorthand[3]}${shorthand[3]}`,16)]}const full=value.match(/^#([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/iu);if(full===null){throw new VectorizeError("invalid_input",`Expected a #rgb or #rrggbb color, received ${JSON.stringify(value)}.`)}return[Number.parseInt(full[1],16),Number.parseInt(full[2],16),Number.parseInt(full[3],16)]}function normalizedHexColor(value){return`#${parseHexColor(value).map((channel)=>channel.toString(16).padStart(2,"0")).join("")}`}function srgbToOklab([red,green,blue]){const linear=(channel)=>{const value=channel/255;return value<=0.04045?value/12.92:((value+0.055)/1.055)**2.4};const r=linear(red);const g=linear(green);const b=linear(blue);const l=Math.cbrt(0.4122214708*r+0.5363325363*g+0.0514459929*b);const m=Math.cbrt(0.2119034982*r+0.6806995451*g+0.1073969566*b);const s=Math.cbrt(0.0883024619*r+0.2817188376*g+0.6299787005*b);return[0.2104542553*l+0.793617785*m-0.0040720468*s,1.9779984951*l-2.428592205*m+0.4505937099*s,0.0259040371*l+0.7827717662*m-0.808675766*s]}function oklabDistance(left,right){return Math.hypot(left[0]-right[0],left[1]-right[1],left[2]-right[2])}function otsuDistanceCutoff(colors,primary){const distances=colors.map(({color,mass})=>({distance:oklabDistance(color,primary),mass}));const maximum=Math.max(...distances.map(({distance})=>distance));if(maximum===0)return Number.POSITIVE_INFINITY;const histogram=Array.from({length:256},()=>({mass:0,moment:0}));for(const{distance,mass}of distances){const index=Math.min(255,Math.floor(distance/maximum*255));histogram[index].mass+=mass;histogram[index].moment+=mass*distance}const totalMass=histogram.reduce((sum,bin)=>sum+bin.mass,0);const totalMoment=histogram.reduce((sum,bin)=>sum+bin.moment,0);let nearMass=0;let nearMoment=0;let bestScore=-1;let bestIndex=254;for(let index=0;index<255;index+=1){const bin=histogram[index];nearMass+=bin.mass;nearMoment+=bin.moment;const farMass=totalMass-nearMass;if(nearMass===0||farMass===0)continue;const nearMean=nearMoment/nearMass;const farMean=(totalMoment-nearMoment)/farMass;const score=nearMass*farMass*(nearMean-farMean)**2;if(score>bestScore){bestScore=score;bestIndex=index}}let cutoff=maximum*(bestIndex+1)/255;const nearShare=()=>distances.reduce((sum,{distance,mass})=>sum+(distance<=cutoff?mass:0),0)/totalMass;if(nearShare()<0.5){let cumulativeMass=0;for(const{distance,mass}of[...distances].sort((left,right)=>left.distance-right.distance)){cumulativeMass+=mass;cutoff=distance;if(cumulativeMass>=totalMass/2)break}}return cutoff}function dominantOklabDuotoneModel(rgba){if(rgba.length===0||rgba.length%4!==0){throw new VectorizeError("invalid_input","A duotone model requires nonempty RGBA pixels.")}const bins=new Map;for(let index=0;index<rgba.length;index+=4){const alpha=rgba[index+3]/255;if(alpha===0)continue;const red=rgba[index];const green=rgba[index+1];const blue=rgba[index+2];const key=red>>4<<8|green>>4<<4|blue>>4;const bin=bins.get(key)??{blue:0,green:0,mass:0,red:0};bin.mass+=alpha;bin.red+=red*alpha;bin.green+=green*alpha;bin.blue+=blue*alpha;bins.set(key,bin)}if(bins.size===0){throw new VectorizeError("invalid_input","A duotone model requires visible pixels.")}const colors=[...bins.entries()].map(([key,bin])=>({color:srgbToOklab([bin.red/bin.mass,bin.green/bin.mass,bin.blue/bin.mass]),key,mass:bin.mass})).sort((left,right)=>right.mass-left.mass||left.key-right.key);const primary=colors[0].color;const cutoff=otsuDistanceCutoff(colors,primary);const totalMass=colors.reduce((sum,{mass})=>sum+mass,0);const primaryMass=colors.reduce((sum,{color,mass})=>sum+(oklabDistance(color,primary)<=cutoff?mass:0),0);return{cutoff,primary,primaryShare:primaryMass/totalMass}}function colorBelongsToPrimary(rgb,model){return oklabDistance(srgbToOklab(rgb),model.primary)<=model.cutoff}var allowedFormats=new Set(["avif","gif","heif","jpeg","png","tiff","webp"]);var METRIC_MAX_EDGE=512;var VECTORIZE_SHARP_CONCURRENCY=1;function configureVectorizeSharpConcurrency(){const actual=sharp.concurrency(VECTORIZE_SHARP_CONCURRENCY);if(actual!==VECTORIZE_SHARP_CONCURRENCY){throw new VectorizeError("trace_failed","The vectorization worker could not bind its Sharp CPU budget.")}}async function loadRaster(input,limits,deadline){deadline.assert("input read");const bytes=await readInputBytes(input,limits.maxInputBytes,deadline);deadline.assert("input metadata");try{const metadata=await sharp(bytes,{failOn:"error",limitInputPixels:limits.maxDecodedPixels,sequentialRead:true}).metadata();const format=metadata.format;if(format===undefined||!allowedFormats.has(format)){throw new VectorizeError("invalid_input",`Expected a supported raster image, received ${format??"an unknown format"}.`)}if((metadata.pages??1)!==1){throw new VectorizeError("invalid_input","Animated and multipage raster inputs are rejected.")}if(metadata.width===undefined||metadata.height===undefined||metadata.width<1||metadata.height<1){throw new VectorizeError("invalid_input","Raster dimensions are missing or invalid.")}assertDimensions(metadata.width,metadata.height,limits);const decoded=await sharp(bytes,{failOn:"error",limitInputPixels:limits.maxDecodedPixels,sequentialRead:true}).rotate().ensureAlpha().raw().toBuffer({resolveWithObject:true});const{channels,height,width}=decoded.info;if(channels!==4){throw new VectorizeError("invalid_input","Raster decoding did not produce RGBA pixels.")}assertDimensions(width,height,limits);const pixels=Uint8Array.from(decoded.data);if(!containsVisiblePixel(pixels)){throw new VectorizeError("invalid_input","A fully transparent image cannot be vectorized.")}deadline.assert("raster decode");const scoreScale=Math.min(1,METRIC_MAX_EDGE/Math.max(width,height));const scoreWidth=Math.max(1,Math.round(width*scoreScale));const scoreHeight=Math.max(1,Math.round(height*scoreScale));const scorePixels=scoreWidth===width&&scoreHeight===height?pixels:Uint8Array.from(await sharp(pixels,{raw:{channels:4,height,width}}).resize(scoreWidth,scoreHeight,{fit:"fill",kernel:"lanczos3"}).raw().toBuffer());deadline.assert("metric sample");return{bytes,format,height,inputBytes:bytes.byteLength,pixels,scoreHeight,scorePixels,scoreWidth,sourceSha256:sha256(bytes),width}}catch(error){if(error instanceof VectorizeError)throw error;throw new VectorizeError("invalid_input","Raster input could not be decoded safely.",{},{cause:error})}}async function readInputBytes(input,maximumBytes,deadline){if(typeof input==="string"){return readRegularInput(resolve(input),maximumBytes,deadline)}const view=input instanceof ArrayBuffer?new Uint8Array(input):input;if(view.byteLength===0){throw new VectorizeError("invalid_input","Raster input is empty.")}if(view.byteLength>maximumBytes){throw new VectorizeError("input_limit",`Raster input exceeds the ${maximumBytes}-byte limit.`,{bytes:view.byteLength,maximumBytes})}deadline.assert("input read");const bytes=Uint8Array.from(view);deadline.assert("input read");return bytes}async function readRegularInput(path,maximumBytes,deadline){let handle;try{deadline.assert("input read");const targetPath=await realpath(path);deadline.assert("input read");handle=await open(targetPath,boundedReadFlags());const metadata=await handle.stat();if(!metadata.isFile()){throw new VectorizeError("invalid_input",`Raster input is not a file: ${path}`)}if(metadata.size<1){throw new VectorizeError("invalid_input","Raster input is empty.")}if(metadata.size>maximumBytes){throw new VectorizeError("input_limit",`Raster input exceeds the ${maximumBytes}-byte limit.`,{bytes:metadata.size,maximumBytes})}const chunk=Buffer.allocUnsafe(Math.min(64*1024,maximumBytes+1));const chunks=[];let bytes=0;while(true){deadline.assert("input read");const maximumRead=Math.min(chunk.byteLength,maximumBytes-bytes+1);const{bytesRead}=await handle.read(chunk,0,maximumRead,null);if(bytesRead===0)break;bytes+=bytesRead;if(bytes>maximumBytes){throw new VectorizeError("input_limit",`Raster input exceeds the ${maximumBytes}-byte limit.`,{bytes,maximumBytes})}chunks.push(Buffer.from(chunk.subarray(0,bytesRead)))}if(bytes===0){throw new VectorizeError("invalid_input","Raster input is empty.")}deadline.assert("input read");return Buffer.concat(chunks,bytes)}catch(error){if(error instanceof VectorizeError)throw error;throw new VectorizeError("invalid_input","Raster input could not be read safely.",{},{cause:error})}finally{await handle?.close().catch(()=>{return})}}function assertDimensions(width,height,limits){if(!Number.isInteger(width)||!Number.isInteger(height)||width<1||height<1||width>limits.maxDimension||height>limits.maxDimension||width*height>limits.maxDecodedPixels){throw new VectorizeError("input_limit",`Raster dimensions must fit ${limits.maxDimension}px and ${limits.maxDecodedPixels} decoded pixels.`,{height,width})}}function containsVisiblePixel(rgba){for(let index=3;index<rgba.length;index+=4){if(rgba[index]>0)return true}return false}function boundedReadFlags(){if(process.platform==="win32")return constants.O_RDONLY;return constants.O_RDONLY|constants.O_NONBLOCK|constants.O_NOFOLLOW}async function encodeTracePng(pixels,width,height){return Uint8Array.from(await sharp(pixels,{raw:{channels:4,height,width}}).png({adaptiveFiltering:false,compressionLevel:9,palette:false}).toBuffer())}async function renderSvgRgba(svg,width,height,maxDecodedPixels){try{return Uint8Array.from(await sharp(Buffer.from(svg),{density:72,failOn:"error",limitInputPixels:maxDecodedPixels}).resize(width,height,{fit:"fill"}).ensureAlpha().raw().toBuffer())}catch(error){throw new VectorizeError("trace_failed","Canonical SVG could not be rendered for quality measurement.",{},{cause:error})}}function sharpProvenance(){const sharpVersions=normalizedPixelToolchain(sharp.versions);const sharpVersion=sharpVersions.sharp;const vipsVersion=sharpVersions.vips;if(sharpVersion===undefined||vipsVersion===undefined){throw new VectorizeError("tool_version","Sharp must report its own and libvips version metadata.")}return{sharp:sharpVersion,sharpVersions,vips:vipsVersion}}function normalizedPixelToolchain(versions){const entries=Object.entries(versions).filter((entry)=>entry[1]!==undefined).sort(([left],[right])=>left.localeCompare(right));if(entries.length===0||entries.some(([name,version])=>name.length===0||version.length===0)){throw new VectorizeError("tool_version","Pixel toolchain versions must be nonempty strings.")}return Object.freeze(Object.fromEntries(entries))}import{AsyncLocalStorage}from"async_hooks";import{constants as constants2}from"fs";import{lstat,mkdtemp,open as open2,rmdir,unlink}from"fs/promises";import{join}from"path";var gatewayCredentialNames=new Set(["AI_GATEWAY_API_KEY","VERCEL_OIDC_TOKEN"]);function nonGatewayChildEnvironment(source=process.env){const environment={...source};for(const name of Object.keys(environment)){if(gatewayCredentialNames.has(name.toLocaleUpperCase("en-US"))){delete environment[name]}}return environment}var MAX_COMMAND_OUTPUT_BYTES=64*1024;var PRIMARY_OUTPUT_READ_BYTES=64*1024;var PRIMARY_OUTPUT_POLL_MS=2;var PRIMARY_OUTPUT_EXIT_EMPTY_POLLS=2;var TERMINATION_GRACE_MS=50;var HARD_KILL_WAIT_MS=500;var MAX_INHERITED_FILE_DESCRIPTORS=16;var timeoutMarker=Symbol("bounded-command-timeout");var noCommandFailure=Symbol("no-command-failure");var activePosixProcessGroups=new Set;var workerTerminationForwardingInstalled=false;var inheritedCommandFileDescriptors=new AsyncLocalStorage;function normalizedInheritedFileDescriptors(value){const descriptors=value??[];if(descriptors.length>MAX_INHERITED_FILE_DESCRIPTORS||descriptors.some((descriptor,index)=>!Number.isSafeInteger(descriptor)||descriptor<0||descriptor>2147483647||descriptors.indexOf(descriptor)!==index)){throw new VectorizeError("invalid_input","Inherited vectorizer descriptors must be unique bounded integers.")}return Object.freeze([...descriptors])}async function withInheritedCommandFileDescriptors(descriptors,callback){const normalized=normalizedInheritedFileDescriptors(descriptors);return await inheritedCommandFileDescriptors.run(normalized,async()=>await callback())}function forwardVectorizeWorkerTermination(){if(process.platform==="win32"||workerTerminationForwardingInstalled){return}workerTerminationForwardingInstalled=true;const signals=[["SIGHUP",129],["SIGINT",130],["SIGTERM",143]];for(const[signal,exitCode]of signals){process.once(signal,()=>{for(const pid of activePosixProcessGroups){safelyKillPosixProcessGroupByPid(pid,"SIGKILL")}process.exit(exitCode)})}}async function runBoundedCommand(command,timeoutMs,failureCode,options={}){return runBoundedCommandInternal(command,timeoutMs,failureCode,options)}async function runBoundedPathOutputCommand(commandForOutput,timeoutMs,failureCode,options){if(process.platform==="win32"){throw new VectorizeError("tool_platform","Bounded pathname output is unavailable on Windows.",{platform:process.platform})}assertPositiveLimit(options.maxOutputBytes,"The command primary-output limit must be positive.");if(timeoutMs<1){throw new VectorizeError("timeout","VTracer exceeded the conversion time limit.")}const startedAt=performance.now();let anchorHandle;let directoryHandle;let outputDirectory;let readerHandle;let result;let failure=noCommandFailure;try{outputDirectory=await mkdtemp(join(options.temporaryRoot,"slopcamera-command-output-"));directoryHandle=await open2(outputDirectory,constants2.O_RDONLY|constants2.O_DIRECTORY|constants2.O_NOFOLLOW);await assertOpenedPrivateDirectory(outputDirectory,directoryHandle);const outputPath=join(outputDirectory,"output.svg");await createPrivateFifo(outputPath,remainingCommandTime(startedAt,timeoutMs),failureCode);anchorHandle=await open2(outputPath,constants2.O_RDWR|constants2.O_NONBLOCK|constants2.O_NOFOLLOW);readerHandle=await open2(outputPath,constants2.O_RDONLY|constants2.O_NONBLOCK|constants2.O_NOFOLLOW);await assertOpenedPrivateFifo(outputPath,anchorHandle,readerHandle);await directoryHandle.chmod(320);const commandResult=await runBoundedCommandInternal(commandForOutput(outputPath),remainingCommandTime(startedAt,timeoutMs),failureCode,options,{handle:readerHandle,maximumBytes:options.maxOutputBytes});if(commandResult.primaryOutput===undefined){throw new VectorizeError(failureCode,"VTracer did not expose its bounded primary output.")}result={output:commandResult.primaryOutput,stderr:commandResult.stderr,stdout:commandResult.stdout}}catch(error){failure=error instanceof VectorizeError?error:executionError(failureCode,error)}const cleanupError=await removePrimaryOutputEndpoint(outputDirectory,outputDirectory===undefined?undefined:join(outputDirectory,"output.svg"),readerHandle,anchorHandle,directoryHandle);if(failure!==noCommandFailure){if(cleanupError===undefined)throw failure;throw new VectorizeError(failure instanceof VectorizeError?failure.code:failureCode,`${failure instanceof Error?failure.message:String(failure)} The temporary primary-output endpoint also could not be removed.`,{...failure instanceof VectorizeError?failure.details:{},cleanup:String(cleanupError)},{cause:failure instanceof Error?failure:undefined})}if(cleanupError!==undefined){throw new VectorizeError(failureCode,"The temporary primary-output endpoint could not be removed.",{cleanup:String(cleanupError)},{cause:cleanupError instanceof Error?cleanupError:undefined})}if(result===undefined){throw new VectorizeError(failureCode,"The bounded command completed without a result.")}return result}async function runBoundedCommandInternal(command,timeoutMs,failureCode,options,primaryOutput){if(command.length===0){throw new VectorizeError("invalid_input","A bounded command requires a command.")}if(timeoutMs<1){throw new VectorizeError("timeout","VTracer exceeded the conversion time limit.")}const maxStdoutBytes=options.maxStdoutBytes??MAX_COMMAND_OUTPUT_BYTES;assertPositiveLimit(maxStdoutBytes,"The command stdout limit must be positive.");const inheritedDescriptors=normalizedInheritedFileDescriptors(options.inheritedFileDescriptors??inheritedCommandFileDescriptors.getStore());let child;const ownsProcessGroup=process.platform!=="win32";try{child=Bun.spawn([...command],{detached:ownsProcessGroup,env:nonGatewayChildEnvironment(),stdio:[options.stdin??"ignore","pipe","pipe",...inheritedDescriptors],windowsHide:true})}catch(error){throw executionError(failureCode,error)}if(process.platform!=="win32"){activePosixProcessGroups.add(child.pid)}const streamAbort=new AbortController;let childHasExited=false;const childExitTask=child.exited.then((exitCode)=>{childHasExited=true;if(ownsProcessGroup&&process.platform!=="win32"){safelyKillPosixProcessGroup(child,"SIGKILL")}return exitCode});const stdoutTask=readBoundedText(child.stdout,maxStdoutBytes,streamAbort.signal,"VTracer emitted too much primary output.");const stderrTask=readBoundedText(child.stderr,MAX_COMMAND_OUTPUT_BYTES,streamAbort.signal,"VTracer emitted too much diagnostic output.");const primaryOutputTask=primaryOutput===undefined?Promise.resolve(undefined):readBoundedFifo(primaryOutput.handle,primaryOutput.maximumBytes,streamAbort.signal,()=>childHasExited);const executionTask=Promise.all([childExitTask,stdoutTask,stderrTask,primaryOutputTask]).then(([exitCode,stdout,stderr,boundedPrimaryOutput])=>{if(exitCode!==0){throw new VectorizeError(failureCode,[`Command failed (${exitCode}): ${command[0]??"unknown"}`,stderr.trim()].filter(Boolean).join(`
|
|
3
3
|
`),{exitCode})}return boundedPrimaryOutput===undefined?{stderr,stdout}:{primaryOutput:boundedPrimaryOutput,stderr,stdout}});let timer;const timeoutTask=new Promise((_resolve,reject)=>{timer=setTimeout(()=>reject(timeoutMarker),timeoutMs)});try{return await Promise.race([executionTask,timeoutTask])}catch(error){let cleanupError;try{await terminateAndWait(child,ownsProcessGroup)}catch(caught){cleanupError=caught}finally{streamAbort.abort();await settlesWithin(Promise.allSettled([stdoutTask,stderrTask,primaryOutputTask]),HARD_KILL_WAIT_MS)}if(error===timeoutMarker){throw new VectorizeError("timeout",cleanupError===undefined?"VTracer exceeded the conversion time limit.":"VTracer exceeded the conversion time limit and did not terminate cleanly.",cleanupError===undefined?{}:{cleanup:String(cleanupError)})}if(error instanceof VectorizeError)throw error;if(cleanupError instanceof VectorizeError)throw cleanupError;throw executionError(failureCode,error)}finally{if(timer!==undefined)clearTimeout(timer);activePosixProcessGroups.delete(child.pid)}}function assertPositiveLimit(value,message){if(!Number.isSafeInteger(value)||value<1){throw new VectorizeError("invalid_input",message)}}function remainingCommandTime(startedAt,timeoutMs){return Math.floor(timeoutMs-(performance.now()-startedAt))}async function createPrivateFifo(path,timeoutMs,failureCode){await runBoundedCommandInternal(["mkfifo","-m","600",path],timeoutMs,failureCode,{})}async function assertOpenedPrivateDirectory(path,directory){const[pathMetadata,openedMetadata]=await Promise.all([lstat(path),directory.stat()]);if(!pathMetadata.isDirectory()||!openedMetadata.isDirectory()||pathMetadata.dev!==openedMetadata.dev||pathMetadata.ino!==openedMetadata.ino){throw new VectorizeError("trace_failed","The bounded primary-output directory changed during setup.")}}async function assertOpenedPrivateFifo(path,anchor,reader){const[pathMetadata,anchorMetadata,readerMetadata]=await Promise.all([lstat(path),anchor.stat(),reader.stat()]);if(!pathMetadata.isFIFO()||!anchorMetadata.isFIFO()||!readerMetadata.isFIFO()||pathMetadata.dev!==anchorMetadata.dev||pathMetadata.ino!==anchorMetadata.ino||anchorMetadata.dev!==readerMetadata.dev||anchorMetadata.ino!==readerMetadata.ino){throw new VectorizeError("trace_failed","The bounded primary-output endpoint changed during setup.")}}async function removePrimaryOutputEndpoint(directoryPath,path,reader,anchor,directoryHandle){let cleanupError;if(directoryPath!==undefined&&directoryHandle===undefined){try{await rmdir(directoryPath)}catch(error){cleanupError??=error}}if(directoryPath!==undefined&&directoryHandle!==undefined){await directoryHandle.chmod(448).catch((error)=>{cleanupError??=error});try{const[pathDirectoryMetadata,openedDirectoryMetadata]=await Promise.all([lstat(directoryPath),directoryHandle.stat()]);if(!pathDirectoryMetadata.isDirectory()||!openedDirectoryMetadata.isDirectory()||pathDirectoryMetadata.dev!==openedDirectoryMetadata.dev||pathDirectoryMetadata.ino!==openedDirectoryMetadata.ino){throw new VectorizeError("trace_failed","The bounded primary-output directory changed before cleanup.")}if(path!==undefined&&cleanupError===undefined){const pathMetadata=await lstat(path).catch((error)=>{if(isFileSystemError(error,"ENOENT"))return;throw error});const openedMetadata=await(reader??anchor)?.stat();if(pathMetadata===undefined&&openedMetadata!==undefined){throw new VectorizeError("trace_failed","The bounded primary-output endpoint disappeared before cleanup.")}if(pathMetadata!==undefined){if(!pathMetadata.isFIFO()||openedMetadata!==undefined&&(pathMetadata.dev!==openedMetadata.dev||pathMetadata.ino!==openedMetadata.ino)){throw new VectorizeError("trace_failed","The bounded primary-output endpoint changed before cleanup.")}await unlink(path)}}if(cleanupError===undefined)await rmdir(directoryPath)}catch(error){cleanupError??=error}}const closes=await Promise.allSettled([reader?.close(),anchor?.close(),directoryHandle?.close()]);cleanupError??=closes.find((result)=>result.status==="rejected")?.reason;return cleanupError}async function terminateAndWait(child,ownsProcessGroup){if(process.platform==="win32"){await killWindowsProcessTree(child.pid)}else if(!ownsProcessGroup){safelyKillChild(child,"SIGTERM");await delay(TERMINATION_GRACE_MS);safelyKillChild(child,"SIGKILL")}else{safelyKillPosixProcessGroup(child,"SIGTERM");await delay(TERMINATION_GRACE_MS);safelyKillPosixProcessGroup(child,"SIGKILL")}if(await settlesWithin(child.exited,HARD_KILL_WAIT_MS))return;throw new VectorizeError("trace_failed","VTracer did not exit after forced termination.")}function safelyKillChild(child,signal){try{child.kill(signal)}catch{}}function safelyKillPosixProcessGroup(child,signal){if(!safelyKillPosixProcessGroupByPid(child.pid,signal)){try{child.kill(signal)}catch{}}}function safelyKillPosixProcessGroupByPid(pid,signal){try{process.kill(-pid,signal);return true}catch{return false}}async function killWindowsProcessTree(pid){let killer;try{killer=Bun.spawn(["taskkill.exe","/PID",String(pid),"/T","/F"],{detached:false,env:nonGatewayChildEnvironment(),stderr:"pipe",stdin:"ignore",stdout:"pipe",windowsHide:true})}catch{return}const drain=Promise.all([readBoundedText(killer.stdout,MAX_COMMAND_OUTPUT_BYTES,AbortSignal.timeout(HARD_KILL_WAIT_MS),"Process-tree cleanup emitted too much output."),readBoundedText(killer.stderr,MAX_COMMAND_OUTPUT_BYTES,AbortSignal.timeout(HARD_KILL_WAIT_MS),"Process-tree cleanup emitted too much output.")]);if(!await settlesWithin(Promise.all([killer.exited,drain]),HARD_KILL_WAIT_MS)){try{killer.kill("SIGKILL")}catch{}}}async function settlesWithin(promise,durationMs){return new Promise((resolve2)=>{let finished=false;const timer=setTimeout(()=>finish(false),durationMs);promise.then(()=>finish(true),()=>finish(true));function finish(settled){if(finished)return;finished=true;clearTimeout(timer);resolve2(settled)}})}async function readBoundedText(stream,maximumBytes,signal,limitMessage){const reader=stream.getReader();const chunks=[];let bytes=0;const cancel=()=>{reader.cancel().catch(()=>{return})};signal.addEventListener("abort",cancel,{once:true});try{while(true){const{done,value}=await reader.read();if(done)break;bytes+=value.byteLength;if(bytes>maximumBytes){await reader.cancel();throw new VectorizeError("output_limit",limitMessage,{bytes,maximumBytes})}chunks.push(value)}return Buffer.concat(chunks,bytes).toString("utf8")}finally{signal.removeEventListener("abort",cancel);reader.releaseLock()}}async function readBoundedFifo(handle,maximumBytes,signal,childHasExited){const buffer=Buffer.allocUnsafe(Math.min(PRIMARY_OUTPUT_READ_BYTES,maximumBytes+1));const chunks=[];let bytes=0;let emptyPollsAfterExit=0;while(!signal.aborted){const maximumRead=Math.min(buffer.byteLength,maximumBytes-bytes+1);try{const{bytesRead}=await handle.read(buffer,0,maximumRead,null);if(bytesRead>0){emptyPollsAfterExit=0;bytes+=bytesRead;if(bytes>maximumBytes){throw new VectorizeError("output_limit","VTracer emitted too much primary output.",{bytes,maximumBytes})}chunks.push(Uint8Array.from(buffer.subarray(0,bytesRead)));continue}}catch(error){if(!isWouldBlockError(error))throw error}if(childHasExited()){emptyPollsAfterExit+=1;if(emptyPollsAfterExit>=PRIMARY_OUTPUT_EXIT_EMPTY_POLLS)break}else{emptyPollsAfterExit=0}await delay(PRIMARY_OUTPUT_POLL_MS)}return Buffer.concat(chunks,bytes).toString("utf8")}function isWouldBlockError(error){return isFileSystemError(error)&&["EAGAIN","EINTR","EWOULDBLOCK"].includes(String(error.code))}function isFileSystemError(error,code){return error instanceof Error&&"code"in error&&(code===undefined||error.code===code)}function delay(durationMs){return new Promise((resolve2)=>{setTimeout(resolve2,durationMs)})}function executionError(failureCode,cause){return new VectorizeError(failureCode,"VTracer could not be executed.",{},{cause:cause instanceof Error?cause:new Error(String(cause))})}import{createHash as createHash2,randomUUID}from"crypto";import{constants as constants3}from"fs";import{chmod,mkdir,open as open3,realpath as realpath2,rename,rm,writeFile}from"fs/promises";import{homedir}from"os";import{dirname,join as join2,resolve as resolve2}from"path";import{gunzipSync,inflateRawSync}from"zlib";var MAX_BINARY_BYTES=8*1024*1024;function extractVTracerArchive(archive,format){return format==="tar.gz"?extractTarEntry(gunzipSync(archive,{maxOutputLength:MAX_BINARY_BYTES}),"vtracer"):extractZipEntry(archive,"vtracer.exe")}function extractTarEntry(tar,expectedName){for(let offset=0;offset+512<=tar.length;){const header=tar.subarray(offset,offset+512);if(header.every((byte)=>byte===0))break;const name=readNullTerminatedAscii(header.subarray(0,100));const prefix=readNullTerminatedAscii(header.subarray(345,500));const fullName=prefix===""?name:`${prefix}/${name}`;const sizeText=readNullTerminatedAscii(header.subarray(124,136)).trim();if(!/^[0-7]+$/u.test(sizeText)){throw new VectorizeError("tool_integrity","VTracer tar contains an invalid size.")}const size=Number.parseInt(sizeText,8);if(!Number.isSafeInteger(size)||size<0||size>MAX_BINARY_BYTES){throw new VectorizeError("tool_integrity","VTracer tar entry exceeds its size limit.")}const contentStart=offset+512;const contentEnd=contentStart+size;if(contentEnd>tar.length){throw new VectorizeError("tool_integrity","VTracer tar entry is truncated.")}if(fullName===expectedName||fullName.endsWith(`/${expectedName}`)){return Uint8Array.from(tar.subarray(contentStart,contentEnd))}offset=contentStart+Math.ceil(size/512)*512}throw new VectorizeError("tool_integrity",`VTracer archive omitted ${expectedName}.`)}function extractZipEntry(zip,expectedName){const bytes=Buffer.from(zip);let offset=0;while(offset+30<=bytes.length){const signature=bytes.readUInt32LE(offset);if(signature!==67324752)break;const flags=bytes.readUInt16LE(offset+6);const compression=bytes.readUInt16LE(offset+8);const compressedSize=bytes.readUInt32LE(offset+18);const uncompressedSize=bytes.readUInt32LE(offset+22);const nameLength=bytes.readUInt16LE(offset+26);const extraLength=bytes.readUInt16LE(offset+28);if((flags&1)!==0||(flags&8)!==0){throw new VectorizeError("tool_integrity","VTracer zip uses encryption or an unsupported data descriptor.")}if(uncompressedSize>MAX_BINARY_BYTES){throw new VectorizeError("tool_integrity","VTracer zip entry exceeds its size limit.")}const nameStart=offset+30;const dataStart=nameStart+nameLength+extraLength;const dataEnd=dataStart+compressedSize;if(dataEnd>bytes.length){throw new VectorizeError("tool_integrity","VTracer zip entry is truncated.")}const name=bytes.subarray(nameStart,nameStart+nameLength).toString("utf8");if(name===expectedName||name.endsWith(`/${expectedName}`)){const compressed=bytes.subarray(dataStart,dataEnd);const extracted=compression===0?Buffer.from(compressed):compression===8?inflateRawSync(compressed,{maxOutputLength:MAX_BINARY_BYTES}):undefined;if(extracted===undefined){throw new VectorizeError("tool_integrity",`VTracer zip uses unsupported compression method ${compression}.`)}if(extracted.length!==uncompressedSize){throw new VectorizeError("tool_integrity","VTracer zip size does not match its header.")}return Uint8Array.from(extracted)}offset=dataEnd}throw new VectorizeError("tool_integrity",`VTracer archive omitted ${expectedName}.`)}function readNullTerminatedAscii(bytes){const end=bytes.indexOf(0);return Buffer.from(end===-1?bytes:bytes.subarray(0,end)).toString("ascii")}var VTRACER_VERSION="0.6.4";var frozenRelease=(release)=>Object.freeze(release);var vtracerReleases=Object.freeze({"darwin-arm64":frozenRelease({archiveSha256:"4a597fd2df8b961d60620df40a7436109427d86e5c028758e6e8796b02d3d996",binarySha256:"77e495bbe212448240387fba3b6d8bc62ba20ecfb6f3c22967e51600f1cc6e66",format:"tar.gz",url:`https://github.com/visioncortex/vtracer/releases/download/${VTRACER_VERSION}/vtracer-aarch64-apple-darwin.tar.gz`}),"darwin-x64":frozenRelease({archiveSha256:"f0d755292c2602d772d63d658a3498b23eca8b5620d4b92a991bd035d5abed16",binarySha256:"0f9f88f989b757e27973a5c4b42665153070183d0787656ee8af2249ab326b78",format:"tar.gz",url:`https://github.com/visioncortex/vtracer/releases/download/${VTRACER_VERSION}/vtracer-x86_64-apple-darwin.tar.gz`}),"linux-arm64":frozenRelease({archiveSha256:"cbd05ad4f491d12dd139ada61485ca1d24db9f981cbe1658632a083cd0ac1a71",binarySha256:"a4b33b6c4066a6b9187802c6efc8b89e211318e12a17164b9d1dd1f29ac5e502",format:"tar.gz",url:`https://github.com/visioncortex/vtracer/releases/download/${VTRACER_VERSION}/vtracer-aarch64-unknown-linux-musl.tar.gz`}),"linux-x64":frozenRelease({archiveSha256:"9290ba0c90e224d6d212836dff5491407c1718bcb72f80b2b5a4a01816df5e40",binarySha256:"6f31499257076bd94de3e976844cf7ca5643f1e194a2bf0599b13f3719452aec",format:"tar.gz",url:`https://github.com/visioncortex/vtracer/releases/download/${VTRACER_VERSION}/vtracer-x86_64-unknown-linux-musl.tar.gz`}),"win32-x64":frozenRelease({archiveSha256:"6b5bc17a6b017129ee40461df254f65d16f3b494c001a8541d41861066b716bf",binarySha256:"4ad8d35e566cd15caf582063b8349bd082b8fa2bd461e99d116fc63ad8fdeca0",format:"zip",url:`https://github.com/visioncortex/vtracer/releases/download/${VTRACER_VERSION}/vtracer-x86_64-pc-windows-msvc.zip`})});var MAX_ARCHIVE_BYTES=4*1024*1024;var MAX_TOOL_BYTES=16*1024*1024;var FILE_CHUNK_BYTES=64*1024;function renamedEnvironmentValue(canonical){return process.env[canonical]}async function ensureVTracer(deadline,privateDirectory,cacheDirectory){const override=renamedEnvironmentValue("SLOPCAMERA_VTRACER_PATH");if(override!==undefined){return copyAndInspectVTracer(resolve2(override),resolve2(privateDirectory),"override",deadline)}const key=`${process.platform}-${process.arch}`;const release=vtracerReleases[key];if(release===undefined){throw new VectorizeError("tool_platform",`VTracer is not pinned for ${process.platform}/${process.arch}.`,{arch:process.arch,platform:process.platform})}const cacheRoot=resolve2(cacheDirectory??defaultCacheDirectory());const suffix=process.platform==="win32"?".exe":"";const toolPath=join2(cacheRoot,"tools",`vtracer-${VTRACER_VERSION}-${process.platform}-${process.arch}${suffix}`);const cachedHash=await hashCachedTool(toolPath,deadline);if(cachedHash!==release.binarySha256){await removeInvalidCachedTool(toolPath);await installOfficialVTracer(toolPath,release,deadline)}return copyAndInspectVTracer(toolPath,resolve2(privateDirectory),"official-release",deadline,release.binarySha256)}function defaultCacheDirectory(){const explicit=renamedEnvironmentValue("SLOPCAMERA_CACHE_DIR");if(explicit!==undefined&&explicit.trim()!=="")return explicit;if(process.platform==="win32"){return join2(process.env.LOCALAPPDATA??homedir(),"slopcamera")}if(process.platform==="darwin")return join2(homedir(),"Library","Caches","slopcamera");return join2(process.env.XDG_CACHE_HOME??join2(homedir(),".cache"),"slopcamera")}async function installOfficialVTracer(toolPath,release,deadline){deadline.assert("VTracer download");const archive=await downloadBounded(release.url,deadline,MAX_ARCHIVE_BYTES);const archiveSha256=sha256(archive);if(archiveSha256!==release.archiveSha256){throw new VectorizeError("tool_integrity",`VTracer archive checksum mismatch: ${archiveSha256}`,{actual:archiveSha256,expected:release.archiveSha256})}const binary=extractVTracerArchive(archive,release.format);if(binary.byteLength<1||binary.byteLength>MAX_TOOL_BYTES){throw new VectorizeError("tool_integrity","VTracer binary exceeds its verified installation limit.",{bytes:binary.byteLength,maximumBytes:MAX_TOOL_BYTES})}const binarySha256=sha256(binary);if(binarySha256!==release.binarySha256){throw new VectorizeError("tool_integrity",`VTracer binary checksum mismatch: ${binarySha256}`,{actual:binarySha256,expected:release.binarySha256})}deadline.assert("VTracer installation");await mkdir(dirname(toolPath),{recursive:true});const stagedPath=`${toolPath}.${randomUUID()}.tmp`;try{await writeFile(stagedPath,binary,{flag:"wx",mode:448});if(process.platform!=="win32")await chmod(stagedPath,320);deadline.assert("VTracer installation");try{await rename(stagedPath,toolPath)}catch(error){const concurrentHash=await hashCachedTool(toolPath,deadline);if(concurrentHash===release.binarySha256){await rm(stagedPath,{force:true})}else{throw error}}}catch(error){await rm(stagedPath,{force:true});if(error instanceof VectorizeError)throw error;throw new VectorizeError("tool_integrity","Could not publish the verified VTracer binary.",{},{cause:error})}const installedHash=await hashCachedTool(toolPath,deadline);if(installedHash!==release.binarySha256){throw new VectorizeError("tool_integrity","Installed VTracer differs from the verified release binary.",{actual:installedHash,expected:release.binarySha256})}}async function copyAndInspectVTracer(sourcePath,privateDirectory,source,deadline,expectedSha256){deadline.assert("VTracer private copy");await mkdir(privateDirectory,{mode:448,recursive:true});const suffix=process.platform==="win32"?".exe":"";const privatePath=join2(privateDirectory,`vtracer-${randomUUID()}${suffix}`);const failureCode=source==="official-release"?"tool_integrity":"tool_version";let sourceHandle;let targetHandle;let copiedSha256;try{const resolvedSourcePath=await realpath2(sourcePath);deadline.assert("VTracer private copy");sourceHandle=await open3(resolvedSourcePath,boundedReadFlags2());const metadata=await sourceHandle.stat();assertBoundedRegularTool(metadata,sourcePath);targetHandle=await open3(privatePath,"wx",320);copiedSha256=await copyAndHash(sourceHandle,targetHandle,MAX_TOOL_BYTES,deadline);await targetHandle.sync();deadline.assert("VTracer private copy")}catch(error){if(error instanceof VectorizeError)throw error;throw new VectorizeError(failureCode,"VTracer could not be copied into the private conversion directory.",{},{cause:error})}finally{await Promise.allSettled([sourceHandle?.close(),targetHandle?.close()])}try{if(copiedSha256.length===0){throw new VectorizeError(failureCode,"VTracer executable is empty.")}if(expectedSha256!==undefined&&copiedSha256!==expectedSha256){throw new VectorizeError("tool_integrity","VTracer changed before it could be copied for conversion.",{actual:copiedSha256,expected:expectedSha256})}if(process.platform!=="win32")await chmod(privatePath,320);const privateSha256=await hashRegularFile(privatePath,MAX_TOOL_BYTES,deadline,failureCode);if(privateSha256!==copiedSha256){throw new VectorizeError("tool_integrity","The private VTracer copy failed its integrity check.",{actual:privateSha256,expected:copiedSha256})}return inspectVTracer(privatePath,source,deadline,copiedSha256,expectedSha256)}catch(error){await rm(privatePath,{force:true});throw error}}async function inspectVTracer(path,source,deadline,copiedSha256,expectedSha256){deadline.assert("VTracer inspection");const{stderr,stdout}=await runBoundedCommand([path,"--version"],deadline.remainingMs(),"tool_version");if(!new RegExp(`\\b${VTRACER_VERSION.replaceAll(".","\\.")}\\b`,"u").test(`${stdout}
|
|
4
|
-
${stderr}`)){throw new VectorizeError("tool_version",`Expected VTracer ${VTRACER_VERSION}.`)}const afterInspectionSha256=await hashRegularFile(path,MAX_TOOL_BYTES,deadline,"tool_integrity");if(afterInspectionSha256!==copiedSha256||expectedSha256!==undefined&&afterInspectionSha256!==expectedSha256){throw new VectorizeError("tool_integrity","The private VTracer executable changed during inspection.",{actual:afterInspectionSha256,expected:expectedSha256??copiedSha256})}return{path,sha256:afterInspectionSha256,source,version:VTRACER_VERSION}}async function hashCachedTool(path,deadline){try{return await hashRegularFile(path,MAX_TOOL_BYTES,deadline,"tool_integrity")}catch(error){if(error instanceof VectorizeError&&error.code==="timeout")throw error;if(isFileSystemError2(error,"ENOENT"))return;return}}async function removeInvalidCachedTool(path){try{await rm(path,{force:true})}catch(error){throw new VectorizeError("tool_integrity","Could not remove an invalid cached VTracer binary.",{},{cause:error})}}async function hashRegularFile(path,maximumBytes,deadline,failureCode){let handle;try{deadline.assert("VTracer hash");const resolvedPath=await realpath2(path);deadline.assert("VTracer hash");handle=await open3(resolvedPath,boundedReadFlags2());const metadata=await handle.stat();if(!metadata
|
|
4
|
+
${stderr}`)){throw new VectorizeError("tool_version",`Expected VTracer ${VTRACER_VERSION}.`)}const afterInspectionSha256=await hashRegularFile(path,MAX_TOOL_BYTES,deadline,"tool_integrity");if(afterInspectionSha256!==copiedSha256||expectedSha256!==undefined&&afterInspectionSha256!==expectedSha256){throw new VectorizeError("tool_integrity","The private VTracer executable changed during inspection.",{actual:afterInspectionSha256,expected:expectedSha256??copiedSha256})}return{path,sha256:afterInspectionSha256,source,version:VTRACER_VERSION}}async function hashCachedTool(path,deadline){try{return await hashRegularFile(path,MAX_TOOL_BYTES,deadline,"tool_integrity")}catch(error){if(error instanceof VectorizeError&&error.code==="timeout")throw error;if(isFileSystemError2(error,"ENOENT"))return;return}}async function removeInvalidCachedTool(path){try{await rm(path,{force:true})}catch(error){throw new VectorizeError("tool_integrity","Could not remove an invalid cached VTracer binary.",{},{cause:error})}}async function hashRegularFile(path,maximumBytes,deadline,failureCode){let handle;try{deadline.assert("VTracer hash");const resolvedPath=await realpath2(path);deadline.assert("VTracer hash");handle=await open3(resolvedPath,boundedReadFlags2());const metadata=await handle.stat();if(!metadata?.isFile()||metadata.size<1||metadata.size>maximumBytes){throw new VectorizeError(failureCode,"VTracer must be a non-empty regular file within its size limit.",{bytes:metadata?.size??0,maximumBytes})}const hash=createHash2("sha256");const buffer=Buffer.allocUnsafe(Math.min(FILE_CHUNK_BYTES,maximumBytes+1));let bytes=0;while(true){deadline.assert("VTracer hash");const maximumRead=Math.min(buffer.byteLength,maximumBytes-bytes+1);const{bytesRead}=await handle.read(buffer,0,maximumRead,null);if(bytesRead===0)break;bytes+=bytesRead;if(bytes>maximumBytes){throw new VectorizeError(failureCode,"VTracer grew beyond its executable size limit while being hashed.",{bytes,maximumBytes})}hash.update(buffer.subarray(0,bytesRead))}deadline.assert("VTracer hash");return hash.digest("hex")}catch(error){if(error instanceof VectorizeError)throw error;throw new VectorizeError(failureCode,"VTracer executable could not be read safely.",{},{cause:error})}finally{await handle?.close().catch(()=>{return})}}async function copyAndHash(source,target,maximumBytes,deadline){const hash=createHash2("sha256");const buffer=Buffer.allocUnsafe(Math.min(FILE_CHUNK_BYTES,maximumBytes+1));let bytes=0;while(true){deadline.assert("VTracer private copy");const maximumRead=Math.min(buffer.byteLength,maximumBytes-bytes+1);const{bytesRead}=await source.read(buffer,0,maximumRead,null);if(bytesRead===0)break;bytes+=bytesRead;if(bytes>maximumBytes){throw new VectorizeError("tool_integrity","VTracer grew beyond its executable size limit while being copied.",{bytes,maximumBytes})}hash.update(buffer.subarray(0,bytesRead));let written=0;while(written<bytesRead){const result=await target.write(buffer,written,bytesRead-written,null);if(result.bytesWritten<1){throw new VectorizeError("tool_integrity","VTracer private copy stopped before completion.")}written+=result.bytesWritten}}return bytes===0?"":hash.digest("hex")}function assertBoundedRegularTool(metadata,path){if(!metadata?.isFile()||metadata.size<1||metadata.size>MAX_TOOL_BYTES){throw new VectorizeError("tool_version",`VTracer must be a non-empty regular file: ${path}`,{bytes:metadata?.size??0,maximumBytes:MAX_TOOL_BYTES})}}async function downloadBounded(url,deadline,maximumBytes){const controller=new AbortController;const timer=setTimeout(()=>controller.abort(),deadline.remainingMs());try{const response=await fetch(url,{headers:{"user-agent":"hraness-slopcamera-vectorizer"},redirect:"follow",signal:controller.signal});if(!response.ok||response.body===null){throw new VectorizeError("tool_download",`Could not download VTracer: ${response.status} ${response.statusText}`)}const declaredLength=Number.parseInt(response.headers.get("content-length")??"",10);if(Number.isFinite(declaredLength)&&declaredLength>maximumBytes){throw new VectorizeError("tool_download","VTracer archive exceeds its download limit.")}const chunks=[];const reader=response.body.getReader();let bytes=0;while(true){deadline.assert("VTracer download");const{done,value}=await reader.read();if(done)break;bytes+=value.byteLength;if(bytes>maximumBytes){await reader.cancel();throw new VectorizeError("tool_download","VTracer archive exceeds its download limit.")}chunks.push(value)}return Uint8Array.from(Buffer.concat(chunks,bytes))}catch(error){if(error instanceof VectorizeError)throw error;if(controller.signal.aborted){throw new VectorizeError("timeout","VTracer download exceeded the conversion time limit.")}throw new VectorizeError("tool_download","Could not download VTracer.",{},{cause:error})}finally{clearTimeout(timer)}}function isFileSystemError2(error,code){return error instanceof Error&&"code"in error&&error.code===code}function boundedReadFlags2(){if(process.platform==="win32")return constants3.O_RDONLY;return constants3.O_RDONLY|constants3.O_NONBLOCK|constants3.O_NOFOLLOW}var VECTORIZE_WORKER_PROTOCOL=1;var MAX_VECTORIZE_REQUEST_BYTES=Math.ceil(vectorizeHardLimits.maxInputBytes/3)*4+512*1024;var MAX_VECTORIZE_RESPONSE_BYTES=vectorizeHardLimits.maxOutputBytes*2+512*1024;import{randomUUID as randomUUID2}from"crypto";import{mkdir as mkdir2,rename as rename2,rm as rm3,writeFile as writeFile2}from"fs/promises";import{dirname as dirname3,join as join4,resolve as resolve3}from"path";var PATH_DATA=/^[MmZzLlHhVvCcSsQqTtAaEe0-9+,.\s-]+$/u;var TRANSLATE=/^translate\(\s*[-+0-9.eE]+\s*(?:,\s*|\s+)[-+0-9.eE]+\s*\)$/u;var DANGEROUS_SOURCE=/<!DOCTYPE|<!ENTITY|<\?(?!xml)|<(?:a|animate|embed|filter|foreignObject|iframe|image|link|object|script|set|style|use)\b|(?:href|src)\s*=|\bon[a-z]+\s*=|url\s*\(/iu;function canonicalizeVTracerSvg(sourceSvg,width,height,maxPaths){if(DANGEROUS_SOURCE.test(sourceSvg)){throw new VectorizeError("unsafe_svg","VTracer output contains active or referenced SVG content.")}const paths=[];const pathPattern=/<path\b([^>]*)\/?\s*>/gu;for(const match of sourceSvg.matchAll(pathPattern)){if(paths.length>=maxPaths){throw new VectorizeError("output_limit",`VTracer output exceeds the ${maxPaths}-path limit.`)}const attributes=match[1];const d=attributes.match(/(?:^|\s)d="([^"]*)"/u)?.[1];const fill=attributes.match(/(?:^|\s)fill="([^"]+)"/u)?.[1];const transform=attributes.match(/(?:^|\s)transform="([^"]+)"/u)?.[1];if(d==="")continue;if(d===undefined||d.includes("&")||!PATH_DATA.test(d)){throw new VectorizeError("unsafe_svg","VTracer output contains unsupported path data.")}if(fill===undefined){throw new VectorizeError("unsafe_svg","VTracer output contains a path without a fill.")}if(transform!==undefined&&!TRANSLATE.test(transform)){throw new VectorizeError("unsafe_svg","VTracer output contains an unsupported path transform.")}const normalizedFill=normalizedHexColor(fill);paths.push({d,fill:normalizedFill,rgb:parseHexColor(normalizedFill),...transform===undefined?{}:{transform}})}if(paths.length===0){throw new VectorizeError("trace_failed","VTracer output did not contain visible paths.")}return{paths,svg:buildColorSvg(paths,width,height)}}function buildColorSvg(paths,width,height,duotone){const palette=duotone===undefined?undefined:[normalizedHexColor(duotone.palette[0]),normalizedHexColor(duotone.palette[1])];const body=paths.map(({d,fill,rgb,transform})=>{const outputFill=duotone===undefined||palette===undefined?fill:colorBelongsToPrimary(rgb,duotone.model)?palette[0]:palette[1];return` <path d="${d}"${transform===undefined?"":` transform="${transform}"`} fill="${outputFill}"/>`});const svg=[`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">`,...body,"</svg>",""].join(`
|
|
5
5
|
`);assertSafeCanonicalSvg(svg);return svg}function buildAlphaMaskedSvg(artworkPaths,maskPaths,width,height,duotone){if(maskPaths.some(({rgb})=>rgb[0]!==rgb[1]||rgb[1]!==rgb[2])){throw new VectorizeError("unsafe_svg","An alpha mask may contain only grayscale fills.")}const palette=duotone===undefined?undefined:[normalizedHexColor(duotone.palette[0]),normalizedHexColor(duotone.palette[1])];const maskFingerprint=maskPaths.map(({d,fill,transform})=>`${d}
|
|
6
6
|
${fill}
|
|
7
7
|
${transform??""}`).join(`
|
|
8
8
|
`);const maskId=`alpha-${sha256(maskFingerprint).slice(0,16)}`;const svg=[`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}">`," <defs>",` <mask id="${maskId}" maskUnits="userSpaceOnUse" maskContentUnits="userSpaceOnUse" x="0" y="0" width="${width}" height="${height}" mask-type="luminance">`,` <rect x="0" y="0" width="${width}" height="${height}" fill="#000000"/>`,...maskPaths.map(({d,fill,transform})=>` <path d="${d}"${transform===undefined?"":` transform="${transform}"`} fill="${fill}"/>`)," </mask>"," </defs>",` <g mask="url(#${maskId})">`,...artworkPaths.map(({d,fill,rgb,transform})=>{const outputFill=duotone===undefined||palette===undefined?fill:colorBelongsToPrimary(rgb,duotone.model)?palette[0]:palette[1];return` <path d="${d}"${transform===undefined?"":` transform="${transform}"`} fill="${outputFill}"/>`})," </g>","</svg>",""].join(`
|
|
9
|
-
`);assertSafeCanonicalSvg(svg);return svg}function countSvgPaths(svg){return svg.match(/<path\b/gu)?.length??0}function assertSafeCanonicalSvg(svg){if(/<!DOCTYPE|<!ENTITY|<\?(?!xml)|<(?:a|animate|embed|filter|foreignObject|iframe|image|link|object|script|set|style|use)\b|(?:href|src)\s*=|\bon[a-z]+\s*=/iu.test(svg)){throw new VectorizeError("unsafe_svg","Canonical SVG contains active or external content.")}for(const match of svg.matchAll(/url\(([^)]+)\)/gu)){if(!/^#[a-z0-9-]+$/u.test(match[1])){throw new VectorizeError("unsafe_svg","Canonical SVG contains an external URL.")}if(!svg.includes(`id="${match[1].slice(1)}"`)){throw new VectorizeError("unsafe_svg","Canonical SVG references an unknown local ID.")}}}import{fileURLToPath}from"url";import{mkdtemp as mkdtemp2,rm as rm2}from"fs/promises";import{tmpdir}from"os";import{dirname as dirname2,join as join3}from"path";var vectorizeErrorCodes=new Set(["input_limit","invalid_input","output_limit","quality_limit","timeout","tool_download","tool_integrity","tool_platform","tool_version","trace_failed","unsafe_svg"]);var WORKER_SHUTDOWN_RESERVE_MS=250;var WORKER_RESPONSE_RESERVE_MS=100;async function runVectorizeWorker(input,options){const startedAt=performance.now();const limits=resolveVectorizeLimits(options.limits);if(process.platform==="win32"){throw new VectorizeError("tool_platform","Bounded VTracer streaming is unavailable on Windows.",{platform:process.platform})}const workerInput=encodeInput(input,limits.maxInputBytes);const temporaryRoot=await mkdtemp2(join3(tmpdir(),"slopcamera-vectorize-"));let result;try{result=await executeVectorizeWorker(workerInput,options,limits,startedAt,temporaryRoot)}finally{await removeTemporaryRoot(temporaryRoot)}if(performance.now()-startedAt>=limits.maxDurationMs){throw new VectorizeError("timeout","Vectorization exceeded the conversion time limit during cleanup.")}return result}async function executeVectorizeWorker(workerInput,options,limits,startedAt,temporaryRoot){const preparationRemainingMs=limits.maxDurationMs-(performance.now()-startedAt);if(preparationRemainingMs<=WORKER_SHUTDOWN_RESERVE_MS+WORKER_RESPONSE_RESERVE_MS){throw new VectorizeError("timeout","Vectorization has no remaining budget for isolated worker execution.")}const workerDurationMs=Math.floor(preparationRemainingMs-WORKER_SHUTDOWN_RESERVE_MS-WORKER_RESPONSE_RESERVE_MS);const inheritedFileDescriptors=inheritedDescriptors(options.inheritedFileDescriptors);const workerInheritedFileDescriptors=inheritedFileDescriptors.map((_descriptor,index)=>index+3);const request={input:workerInput,options:cloneOptions(options,workerDurationMs,workerInheritedFileDescriptors),protocol:VECTORIZE_WORKER_PROTOCOL,temporaryRoot};const requestBytes=Buffer.from(JSON.stringify(request));if(requestBytes.byteLength>MAX_VECTORIZE_REQUEST_BYTES){throw new VectorizeError("input_limit","The vectorization worker request exceeds its IPC limit.",{bytes:requestBytes.byteLength,maximumBytes:MAX_VECTORIZE_REQUEST_BYTES})}const remainingMs=limits.maxDurationMs-(performance.now()-startedAt);if(remainingMs<=WORKER_SHUTDOWN_RESERVE_MS){throw new VectorizeError("timeout","Vectorization has no remaining budget for isolated worker startup and cleanup.")}const{stdout}=await runBoundedCommand([process.execPath,workerEntryPath()],Math.floor(remainingMs-WORKER_SHUTDOWN_RESERVE_MS),"trace_failed",{...inheritedFileDescriptors.length===0?{}:{inheritedFileDescriptors},maxStdoutBytes:MAX_VECTORIZE_RESPONSE_BYTES,stdin:requestBytes});if(performance.now()-startedAt>=limits.maxDurationMs){throw new VectorizeError("timeout","Vectorization exceeded the conversion time limit.")}const response=parseResponse(stdout);if(!response.ok){throw new VectorizeError(response.error.code,response.error.message,response.error.details)}assertResult(response.result,limits.maxOutputBytes);if(performance.now()-startedAt>=limits.maxDurationMs){throw new VectorizeError("timeout","Vectorization exceeded the conversion time limit.")}return response.result}async function removeTemporaryRoot(temporaryRoot){try{await rm2(temporaryRoot,{force:true,recursive:true})}catch(error){throw new VectorizeError("trace_failed","The isolated vectorization directory could not be removed.",{temporaryRoot},{cause:error})}}function encodeInput(input,maximumInputBytes){return typeof input==="string"?{kind:"path",value:input}:encodeBytes(input,maximumInputBytes)}function encodeBytes(input,maximumInputBytes){const view=input instanceof ArrayBuffer?new Uint8Array(input):input;if(view.byteLength<1){throw new VectorizeError("invalid_input","Raster input is empty.")}if(view.byteLength>maximumInputBytes){throw new VectorizeError("input_limit",`Raster input exceeds the ${maximumInputBytes}-byte limit.`,{bytes:view.byteLength,maximumBytes:maximumInputBytes})}return{kind:"bytes",value:Buffer.from(view.buffer,view.byteOffset,view.byteLength).toString("base64")}}function cloneOptions(options,workerDurationMs,inheritedFileDescriptors){return{...options.alphaCutoff===undefined?{}:{alphaCutoff:options.alphaCutoff},...options.cacheDirectory===undefined?{}:{cacheDirectory:options.cacheDirectory},...options.duotone===undefined?{}:{duotone:[options.duotone[0],options.duotone[1]]},...inheritedFileDescriptors.length===0?{}:{inheritedFileDescriptors},limits:{...options.limits,maxDurationMs:workerDurationMs},...options.outputPath===undefined?{}:{outputPath:options.outputPath}}}function inheritedDescriptors(value){const descriptors=value??[];if(descriptors.length>16||descriptors.some((descriptor,index)=>!Number.isSafeInteger(descriptor)||descriptor<0||descriptor>2147483647||descriptors.indexOf(descriptor)!==index)){throw new VectorizeError("invalid_input","Inherited vectorizer descriptors must be unique bounded integers.")}return Object.freeze([...descriptors])}function workerEntryPath(){const modulePath=fileURLToPath(import.meta.url);return modulePath.endsWith(".ts")?join3(dirname2(modulePath),"worker.ts"):join3(dirname2(modulePath),"vectorize","worker.js")}function parseResponse(stdout){let parsed;try{parsed=JSON.parse(stdout)}catch(error){throw new VectorizeError("trace_failed","The vectorization worker returned malformed output.",{},{cause:error})}if(!isRecord(parsed)||parsed.protocol!==VECTORIZE_WORKER_PROTOCOL){throw new VectorizeError("trace_failed","The vectorization worker returned an incompatible response.")}if(parsed.ok===true&&isVectorizeResult(parsed.result)){return parsed}if(parsed.ok===false&&isRecord(parsed.error)&&typeof parsed.error.code==="string"&&vectorizeErrorCodes.has(parsed.error.code)&&typeof parsed.error.message==="string"&&isRecord(parsed.error.details)){return parsed}throw new VectorizeError("trace_failed","The vectorization worker returned an invalid response.")}function assertResult(result,maximumOutputBytes){const bytes=Buffer.byteLength(result.svg);if(bytes<1||bytes>maximumOutputBytes||result.receipt.bytes!==bytes||result.receipt.svgSha256.length!==64){throw new VectorizeError("trace_failed","The vectorization worker response violates its output contract.",{bytes,maximumOutputBytes})}}function isVectorizeResult(value){if(!isRecord(value)||value.outputPath!==null&&typeof value.outputPath!=="string"||typeof value.svg!=="string"||!isRecord(value.receipt)){return false}const profile=value.receipt.profile;return typeof value.receipt.bytes==="number"&&typeof value.receipt.svgSha256==="string"&&typeof profile==="string"&&vectorizeProfileNames.includes(profile)}function isRecord(value){return typeof value==="object"&&value!==null&&!Array.isArray(value)}var DEFAULT_ALPHA_CUTOFF=8;var COMPARE_LOW_ALPHA_MASS_RATIO=0.002;var profiles=[{args:["--colormode","color","--hierarchical","stacked","--mode","spline","--filter_speckle","0","--color_precision","8","--gradient_step","4","--segment_length","4.5","--path_precision","4"],name:"balanced"},{args:["--colormode","color","--hierarchical","stacked","--mode","spline","--filter_speckle","0","--color_precision","8","--gradient_step","2","--segment_length","3.5","--path_precision","5"],name:"detailed"},{args:["--preset","photo","--hierarchical","stacked","--filter_speckle","0"],name:"photo"}];var alphaMaskArgs=["--colormode","color","--hierarchical","cutout","--mode","spline","--filter_speckle","0","--color_precision","8","--gradient_step","2","--segment_length","3.5","--path_precision","5","--corner_threshold","30","--splice_threshold","20"];function vectorizeImage(input,options={}){return runVectorizeWorker(input,options)}async function vectorizeImageInProcess(input,options={},temporaryRoot){const limits=resolveVectorizeLimits(options.limits);const deadline=new VectorizeDeadline(limits.maxDurationMs);const alphaCutoff=options.alphaCutoff??DEFAULT_ALPHA_CUTOFF;if(!Number.isInteger(alphaCutoff)||alphaCutoff<1||alphaCutoff>64){throw new VectorizeError("invalid_input","alphaCutoff must be an integer from 1 through 64.")}const duotonePalette=options.duotone===undefined?undefined:[normalizedHexColor(options.duotone[0]),normalizedHexColor(options.duotone[1])];const raster=await loadRaster(input,limits,deadline);if(process.platform==="win32"){throw new VectorizeError("tool_platform","Bounded VTracer streaming is unavailable on Windows.",{platform:process.platform})}const tool=await ensureVTracer(deadline,temporaryRoot,options.cacheDirectory);const errors=[];const candidates=[];const variations=traceVariations(raster.pixels,alphaCutoff);for(const variation of variations){deadline.assert(`${variation.name} trace`);const sourcePath=join4(temporaryRoot,`${variation.name}.png`);await writeFile2(sourcePath,await encodeTracePng(variation.pixels,raster.width,raster.height));const balanced=await attemptCandidate(raster,variation,profiles[0],sourcePath,tool,temporaryRoot,limits,deadline,errors);if(balanced!==undefined)candidates.push(balanced);if(balanced!==undefined&&passesFastQuality(balanced.quality))continue;const detailed=await attemptCandidate(raster,variation,profiles[1],sourcePath,tool,temporaryRoot,limits,deadline,errors);if(detailed!==undefined)candidates.push(detailed);if([balanced,detailed].some((candidate)=>candidate!==undefined&&passesQuality(candidate.quality))){continue}const photo=await attemptCandidate(raster,variation,profiles[2],sourcePath,tool,temporaryRoot,limits,deadline,errors);if(photo!==undefined)candidates.push(photo)}const baseForMask=[...candidates].sort(compareFidelity)[0];if(baseForMask!==undefined&&hasFractionalAlpha(raster.pixels)&&baseForMask.quality.alphaRmse>0.06){const masked=await attemptAlphaMask(raster,baseForMask,tool,temporaryRoot,limits,deadline,errors);if(masked!==undefined)candidates.push(masked)}const eligible=candidates.filter((candidate)=>candidate.bytes<=limits.maxOutputBytes&&candidate.pathCount<=limits.maxPaths&&passesQuality(candidate.quality));if(eligible.length===0){throw new VectorizeError("quality_limit","No adaptive vector candidate passed the fidelity and output gates.",{candidates:candidates.map(candidateSummary),errors})}const selected=selectCandidate(eligible);const duotone=duotonePalette===undefined?undefined:{model:dominantOklabDuotoneModel(raster.pixels),palette:duotonePalette};const svg=selected.maskPaths===undefined?buildColorSvg(selected.artworkPaths,raster.width,raster.height,duotone):buildAlphaMaskedSvg(selected.artworkPaths,selected.maskPaths,raster.width,raster.height,duotone);assertSafeCanonicalSvg(svg);const bytes=Buffer.byteLength(svg);const pathCount=countSvgPaths(svg);if(bytes>limits.maxOutputBytes||pathCount>limits.maxPaths){throw new VectorizeError("output_limit",`Canonical SVG exceeds ${limits.maxOutputBytes} bytes or ${limits.maxPaths} paths.`,{bytes,pathCount})}deadline.assert("output publication");const outputPath=options.outputPath===undefined?null:await writeSvgAtomically(options.outputPath,svg);const pixelToolchain=sharpProvenance();return{outputPath,receipt:{alphaCutoff:selected.alphaCutoff,bytes,candidatesEvaluated:candidates.length,format:raster.format,height:raster.height,inputBytes:raster.inputBytes,outputMode:duotone===undefined?"color":"duotone",pathCount,profile:selected.profile,provenance:{arch:process.arch,platform:process.platform,sharp:pixelToolchain.sharp,sharpVersions:pixelToolchain.sharpVersions,vips:pixelToolchain.vips,vtracerSha256:tool.sha256,vtracerSource:tool.source,vtracerVersion:tool.version},quality:roundedQuality(selected.quality),receiptVersion:1,representation:selected.representation,sourceSha256:raster.sourceSha256,svgSha256:sha256(svg),width:raster.width},svg}}function traceVariations(pixels,alphaCutoff){const sanitized=sanitizedTraceRgba(pixels,alphaCutoff);const output=[];if(containsVisiblePixel2(sanitized)){output.push({alphaCutoff,name:`alpha-${alphaCutoff}`,pixels:sanitized})}if(alphaCutoff>1&&(output.length===0||lowAlphaMassRatio(pixels,alphaCutoff)>=COMPARE_LOW_ALPHA_MASS_RATIO)){output.push({alphaCutoff:1,name:"alpha-1",pixels:sanitizedTraceRgba(pixels,1)})}if(output.length===0){throw new VectorizeError("invalid_input","Alpha sanitation removed every visible pixel.")}return output}function containsVisiblePixel2(pixels){for(let index=3;index<pixels.length;index+=4){if(pixels[index]>0)return true}return false}async function attemptCandidate(raster,variation,profile,sourcePath,tool,temporaryRoot,limits,deadline,errors){try{const raw=await traceRawSvg(tool,sourcePath,profile.args,temporaryRoot,limits.maxOutputBytes,deadline);deadline.assert(`${profile.name} trace`);const canonical=canonicalizeVTracerSvg(raw,raster.width,raster.height,limits.maxPaths);const bytes=Buffer.byteLength(canonical.svg);const pathCount=canonical.paths.length;if(bytes>limits.maxOutputBytes||pathCount>limits.maxPaths){throw new VectorizeError("output_limit",`${profile.name} exceeds vector output limits.`)}const quality=await scoreSvg(canonical.svg,raster,limits.maxDecodedPixels,deadline);return{alphaCutoff:variation.alphaCutoff,artworkPaths:canonical.paths,bytes,pathCount,profile:profile.name,quality,representation:"color-paths",svg:canonical.svg}}catch(error){if(error instanceof VectorizeError&&error.code==="timeout")throw error;errors.push(`${variation.name}/${profile.name}: ${error instanceof Error?error.message:String(error)}`);return}}async function attemptAlphaMask(raster,base,tool,temporaryRoot,limits,deadline,errors){try{const alphaPixels=alphaPlaneTraceRgba(raster.pixels);const sourcePath=join4(temporaryRoot,"alpha-plane.png");await writeFile2(sourcePath,await encodeTracePng(alphaPixels,raster.width,raster.height));const raw=await traceRawSvg(tool,sourcePath,alphaMaskArgs,temporaryRoot,limits.maxOutputBytes,deadline);const mask=canonicalizeVTracerSvg(raw,raster.width,raster.height,limits.maxPaths);const pathCount=base.artworkPaths.length+mask.paths.length;if(pathCount>limits.maxPaths){throw new VectorizeError("output_limit","Alpha-masked output exceeds the path limit.")}const svg=buildAlphaMaskedSvg(base.artworkPaths,mask.paths,raster.width,raster.height);const bytes=Buffer.byteLength(svg);if(bytes>limits.maxOutputBytes){throw new VectorizeError("output_limit","Alpha-masked output exceeds the byte limit.")}const quality=await scoreSvg(svg,raster,limits.maxDecodedPixels,deadline);return{alphaCutoff:base.alphaCutoff,artworkPaths:base.artworkPaths,bytes,maskPaths:mask.paths,pathCount,profile:base.profile,quality,representation:"alpha-mask",svg}}catch(error){if(error instanceof VectorizeError&&error.code==="timeout")throw error;errors.push(`alpha-mask: ${error instanceof Error?error.message:String(error)}`);return}}async function scoreSvg(svg,raster,maxDecodedPixels,deadline){const candidatePixels=await renderSvgRgba(svg,raster.scoreWidth,raster.scoreHeight,maxDecodedPixels);deadline.assert("quality measurement");const support=measureSupport(raster.scorePixels,candidatePixels,raster.scoreWidth,raster.scoreHeight,1);return{alphaRmse:normalizedAlphaRmse(raster.scorePixels,candidatePixels),colorRmse:normalizedPremultipliedRmse(raster.scorePixels,candidatePixels),outsideAlphaRatio:support.outsideAlphaRatio,sampleHeight:raster.scoreHeight,sampleWidth:raster.scoreWidth,supportRecall:support.supportRecall}}async function traceRawSvg(tool,sourcePath,args,temporaryRoot,maximumBytes,deadline){const{output}=await runBoundedPathOutputCommand((outputPath)=>[tool.path,"--input",sourcePath,"--output",outputPath,...args],deadline.remainingMs(),"trace_failed",{maxOutputBytes:maximumBytes,temporaryRoot});if(output.length===0){throw new VectorizeError("trace_failed","VTracer did not emit an SVG.")}return output}function passesFastQuality(quality){return passesQuality(quality)&&quality.colorRmse<=0.12&&quality.alphaRmse<=0.18&&quality.outsideAlphaRatio<=0.02&&quality.supportRecall>=0.97}function passesQuality(quality){return quality.colorRmse<=0.3&&quality.alphaRmse<=0.3&&quality.outsideAlphaRatio<=0.15&&quality.supportRecall>=0.8}function compareFidelity(left,right){return left.quality.colorRmse-right.quality.colorRmse||left.quality.alphaRmse-right.quality.alphaRmse||left.bytes-right.bytes||left.pathCount-right.pathCount||left.profile.localeCompare(right.profile)}function selectCandidate(candidates){const bestRmse=Math.min(...candidates.map(({quality})=>quality.colorRmse));const fidelityWindow=candidates.filter(({quality})=>quality.colorRmse<=bestRmse+0.005);return[...fidelityWindow].sort((left,right)=>left.bytes-right.bytes||left.pathCount-right.pathCount||left.quality.alphaRmse-right.quality.alphaRmse||left.profile.localeCompare(right.profile)||left.representation.localeCompare(right.representation))[0]}function roundedQuality(quality){const rounded=(value)=>Number(value.toFixed(8));return{alphaRmse:rounded(quality.alphaRmse),colorRmse:rounded(quality.colorRmse),outsideAlphaRatio:rounded(quality.outsideAlphaRatio),sampleHeight:quality.sampleHeight,sampleWidth:quality.sampleWidth,supportRecall:rounded(quality.supportRecall)}}function candidateSummary(candidate){return{alphaCutoff:candidate.alphaCutoff,bytes:candidate.bytes,pathCount:candidate.pathCount,profile:candidate.profile,quality:roundedQuality(candidate.quality),representation:candidate.representation}}async function writeSvgAtomically(path,svg){const outputPath=resolve3(path);await mkdir2(dirname3(outputPath),{recursive:true});const temporaryPath=`${outputPath}.${randomUUID2()}.tmp`;try{await writeFile2(temporaryPath,svg,{flag:"wx"});await rename2(temporaryPath,outputPath)}catch(error){await rm3(temporaryPath,{force:true});throw new VectorizeError("output_limit",`Could not atomically write ${outputPath}.`,{},{cause:error})}return outputPath}
|
|
9
|
+
`);assertSafeCanonicalSvg(svg);return svg}function countSvgPaths(svg){return svg.match(/<path\b/gu)?.length??0}function assertSafeCanonicalSvg(svg){if(/<!DOCTYPE|<!ENTITY|<\?(?!xml)|<(?:a|animate|embed|filter|foreignObject|iframe|image|link|object|script|set|style|use)\b|(?:href|src)\s*=|\bon[a-z]+\s*=/iu.test(svg)){throw new VectorizeError("unsafe_svg","Canonical SVG contains active or external content.")}for(const match of svg.matchAll(/url\(([^)]+)\)/gu)){if(!/^#[a-z0-9-]+$/u.test(match[1])){throw new VectorizeError("unsafe_svg","Canonical SVG contains an external URL.")}if(!svg.includes(`id="${match[1].slice(1)}"`)){throw new VectorizeError("unsafe_svg","Canonical SVG references an unknown local ID.")}}}import{fileURLToPath}from"url";import{mkdtemp as mkdtemp2,rm as rm2}from"fs/promises";import{tmpdir}from"os";import{dirname as dirname2,join as join3}from"path";var vectorizeErrorCodes=new Set(["input_limit","invalid_input","output_limit","quality_limit","timeout","tool_download","tool_integrity","tool_platform","tool_version","trace_failed","unsafe_svg"]);var PROCESS_TEARDOWN_ALLOWANCE_MS=250;var TERMINATION_LAYERS=2;var WORKER_SHUTDOWN_RESERVE_MS=TERMINATION_LAYERS*(TERMINATION_GRACE_MS+PROCESS_TEARDOWN_ALLOWANCE_MS);var WORKER_RESPONSE_RESERVE_MS=150;async function runVectorizeWorker(input,options){const startedAt=performance.now();const limits=resolveVectorizeLimits(options.limits);if(process.platform==="win32"){throw new VectorizeError("tool_platform","Bounded VTracer streaming is unavailable on Windows.",{platform:process.platform})}const workerInput=encodeInput(input,limits.maxInputBytes);const temporaryRoot=await mkdtemp2(join3(tmpdir(),"slopcamera-vectorize-"));let result;try{result=await executeVectorizeWorker(workerInput,options,limits,startedAt,temporaryRoot)}finally{await removeTemporaryRoot(temporaryRoot)}if(performance.now()-startedAt>=limits.maxDurationMs){throw new VectorizeError("timeout","Vectorization exceeded the conversion time limit during cleanup.")}return result}async function executeVectorizeWorker(workerInput,options,limits,startedAt,temporaryRoot){const preparationRemainingMs=limits.maxDurationMs-(performance.now()-startedAt);if(preparationRemainingMs<=WORKER_SHUTDOWN_RESERVE_MS+WORKER_RESPONSE_RESERVE_MS){throw new VectorizeError("timeout","Vectorization has no remaining budget for isolated worker execution.")}const workerDurationMs=Math.floor(preparationRemainingMs-WORKER_SHUTDOWN_RESERVE_MS-WORKER_RESPONSE_RESERVE_MS);const inheritedFileDescriptors=inheritedDescriptors(options.inheritedFileDescriptors);const workerInheritedFileDescriptors=inheritedFileDescriptors.map((_descriptor,index)=>index+3);const request={input:workerInput,options:cloneOptions(options,workerDurationMs,workerInheritedFileDescriptors),protocol:VECTORIZE_WORKER_PROTOCOL,temporaryRoot};const requestBytes=Buffer.from(JSON.stringify(request));if(requestBytes.byteLength>MAX_VECTORIZE_REQUEST_BYTES){throw new VectorizeError("input_limit","The vectorization worker request exceeds its IPC limit.",{bytes:requestBytes.byteLength,maximumBytes:MAX_VECTORIZE_REQUEST_BYTES})}const remainingMs=limits.maxDurationMs-(performance.now()-startedAt);if(remainingMs<=WORKER_SHUTDOWN_RESERVE_MS){throw new VectorizeError("timeout","Vectorization has no remaining budget for isolated worker startup and cleanup.")}const{stdout}=await runBoundedCommand([process.execPath,workerEntryPath()],Math.floor(remainingMs-WORKER_SHUTDOWN_RESERVE_MS),"trace_failed",{...inheritedFileDescriptors.length===0?{}:{inheritedFileDescriptors},maxStdoutBytes:MAX_VECTORIZE_RESPONSE_BYTES,stdin:requestBytes});if(performance.now()-startedAt>=limits.maxDurationMs){throw new VectorizeError("timeout","Vectorization exceeded the conversion time limit.")}const response=parseResponse(stdout);if(!response.ok){throw new VectorizeError(response.error.code,response.error.message,response.error.details)}assertResult(response.result,limits.maxOutputBytes);if(performance.now()-startedAt>=limits.maxDurationMs){throw new VectorizeError("timeout","Vectorization exceeded the conversion time limit.")}return response.result}async function removeTemporaryRoot(temporaryRoot){try{await rm2(temporaryRoot,{force:true,recursive:true})}catch(error){throw new VectorizeError("trace_failed","The isolated vectorization directory could not be removed.",{temporaryRoot},{cause:error})}}function encodeInput(input,maximumInputBytes){return typeof input==="string"?{kind:"path",value:input}:encodeBytes(input,maximumInputBytes)}function encodeBytes(input,maximumInputBytes){const view=input instanceof ArrayBuffer?new Uint8Array(input):input;if(view.byteLength<1){throw new VectorizeError("invalid_input","Raster input is empty.")}if(view.byteLength>maximumInputBytes){throw new VectorizeError("input_limit",`Raster input exceeds the ${maximumInputBytes}-byte limit.`,{bytes:view.byteLength,maximumBytes:maximumInputBytes})}return{kind:"bytes",value:Buffer.from(view.buffer,view.byteOffset,view.byteLength).toString("base64")}}function cloneOptions(options,workerDurationMs,inheritedFileDescriptors){return{...options.alphaCutoff===undefined?{}:{alphaCutoff:options.alphaCutoff},...options.cacheDirectory===undefined?{}:{cacheDirectory:options.cacheDirectory},...options.duotone===undefined?{}:{duotone:[options.duotone[0],options.duotone[1]]},...inheritedFileDescriptors.length===0?{}:{inheritedFileDescriptors},limits:{...options.limits,maxDurationMs:workerDurationMs},...options.outputPath===undefined?{}:{outputPath:options.outputPath}}}function inheritedDescriptors(value){const descriptors=value??[];if(descriptors.length>16||descriptors.some((descriptor,index)=>!Number.isSafeInteger(descriptor)||descriptor<0||descriptor>2147483647||descriptors.indexOf(descriptor)!==index)){throw new VectorizeError("invalid_input","Inherited vectorizer descriptors must be unique bounded integers.")}return Object.freeze([...descriptors])}function workerEntryPath(){const modulePath=fileURLToPath(import.meta.url);return modulePath.endsWith(".ts")?join3(dirname2(modulePath),"worker.ts"):join3(dirname2(modulePath),"vectorize","worker.js")}function parseResponse(stdout){let parsed;try{parsed=JSON.parse(stdout)}catch(error){throw new VectorizeError("trace_failed","The vectorization worker returned malformed output.",{},{cause:error})}if(!isRecord(parsed)||parsed.protocol!==VECTORIZE_WORKER_PROTOCOL){throw new VectorizeError("trace_failed","The vectorization worker returned an incompatible response.")}if(parsed.ok===true&&isVectorizeResult(parsed.result)){return parsed}if(parsed.ok===false&&isRecord(parsed.error)&&typeof parsed.error.code==="string"&&vectorizeErrorCodes.has(parsed.error.code)&&typeof parsed.error.message==="string"&&isRecord(parsed.error.details)){return parsed}throw new VectorizeError("trace_failed","The vectorization worker returned an invalid response.")}function assertResult(result,maximumOutputBytes){const bytes=Buffer.byteLength(result.svg);if(bytes<1||bytes>maximumOutputBytes||result.receipt.bytes!==bytes||result.receipt.svgSha256.length!==64){throw new VectorizeError("trace_failed","The vectorization worker response violates its output contract.",{bytes,maximumOutputBytes})}}function isVectorizeResult(value){if(!isRecord(value)||value.outputPath!==null&&typeof value.outputPath!=="string"||typeof value.svg!=="string"||!isRecord(value.receipt)){return false}const profile=value.receipt.profile;return typeof value.receipt.bytes==="number"&&typeof value.receipt.svgSha256==="string"&&typeof profile==="string"&&vectorizeProfileNames.includes(profile)}function isRecord(value){return typeof value==="object"&&value!==null&&!Array.isArray(value)}var DEFAULT_ALPHA_CUTOFF=8;var COMPARE_LOW_ALPHA_MASS_RATIO=0.002;var profiles=[{args:["--colormode","color","--hierarchical","stacked","--mode","spline","--filter_speckle","0","--color_precision","8","--gradient_step","4","--segment_length","4.5","--path_precision","4"],name:"balanced"},{args:["--colormode","color","--hierarchical","stacked","--mode","spline","--filter_speckle","0","--color_precision","8","--gradient_step","2","--segment_length","3.5","--path_precision","5"],name:"detailed"},{args:["--preset","photo","--hierarchical","stacked","--filter_speckle","0"],name:"photo"}];var alphaMaskArgs=["--colormode","color","--hierarchical","cutout","--mode","spline","--filter_speckle","0","--color_precision","8","--gradient_step","2","--segment_length","3.5","--path_precision","5","--corner_threshold","30","--splice_threshold","20"];function vectorizeImage(input,options={}){return runVectorizeWorker(input,options)}async function vectorizeImageInProcess(input,options={},temporaryRoot){const limits=resolveVectorizeLimits(options.limits);const deadline=new VectorizeDeadline(limits.maxDurationMs);const alphaCutoff=options.alphaCutoff??DEFAULT_ALPHA_CUTOFF;if(!Number.isInteger(alphaCutoff)||alphaCutoff<1||alphaCutoff>64){throw new VectorizeError("invalid_input","alphaCutoff must be an integer from 1 through 64.")}const duotonePalette=options.duotone===undefined?undefined:[normalizedHexColor(options.duotone[0]),normalizedHexColor(options.duotone[1])];const raster=await loadRaster(input,limits,deadline);if(process.platform==="win32"){throw new VectorizeError("tool_platform","Bounded VTracer streaming is unavailable on Windows.",{platform:process.platform})}const tool=await ensureVTracer(deadline,temporaryRoot,options.cacheDirectory);const errors=[];const candidates=[];const variations=traceVariations(raster.pixels,alphaCutoff);for(const variation of variations){deadline.assert(`${variation.name} trace`);const sourcePath=join4(temporaryRoot,`${variation.name}.png`);await writeFile2(sourcePath,await encodeTracePng(variation.pixels,raster.width,raster.height));const balanced=await attemptCandidate(raster,variation,profiles[0],sourcePath,tool,temporaryRoot,limits,deadline,errors);if(balanced!==undefined)candidates.push(balanced);if(balanced!==undefined&&passesFastQuality(balanced.quality))continue;const detailed=await attemptCandidate(raster,variation,profiles[1],sourcePath,tool,temporaryRoot,limits,deadline,errors);if(detailed!==undefined)candidates.push(detailed);if([balanced,detailed].some((candidate)=>candidate!==undefined&&passesQuality(candidate.quality))){continue}const photo=await attemptCandidate(raster,variation,profiles[2],sourcePath,tool,temporaryRoot,limits,deadline,errors);if(photo!==undefined)candidates.push(photo)}const baseForMask=[...candidates].sort(compareFidelity)[0];if(baseForMask!==undefined&&hasFractionalAlpha(raster.pixels)&&baseForMask.quality.alphaRmse>0.06){const masked=await attemptAlphaMask(raster,baseForMask,tool,temporaryRoot,limits,deadline,errors);if(masked!==undefined)candidates.push(masked)}const eligible=candidates.filter((candidate)=>candidate.bytes<=limits.maxOutputBytes&&candidate.pathCount<=limits.maxPaths&&passesQuality(candidate.quality));if(eligible.length===0){throw new VectorizeError("quality_limit","No adaptive vector candidate passed the fidelity and output gates.",{candidates:candidates.map(candidateSummary),errors})}const selected=selectCandidate(eligible);const duotone=duotonePalette===undefined?undefined:{model:dominantOklabDuotoneModel(raster.pixels),palette:duotonePalette};const svg=selected.maskPaths===undefined?buildColorSvg(selected.artworkPaths,raster.width,raster.height,duotone):buildAlphaMaskedSvg(selected.artworkPaths,selected.maskPaths,raster.width,raster.height,duotone);assertSafeCanonicalSvg(svg);const bytes=Buffer.byteLength(svg);const pathCount=countSvgPaths(svg);if(bytes>limits.maxOutputBytes||pathCount>limits.maxPaths){throw new VectorizeError("output_limit",`Canonical SVG exceeds ${limits.maxOutputBytes} bytes or ${limits.maxPaths} paths.`,{bytes,pathCount})}deadline.assert("output publication");const outputPath=options.outputPath===undefined?null:await writeSvgAtomically(options.outputPath,svg);const pixelToolchain=sharpProvenance();return{outputPath,receipt:{alphaCutoff:selected.alphaCutoff,bytes,candidatesEvaluated:candidates.length,format:raster.format,height:raster.height,inputBytes:raster.inputBytes,outputMode:duotone===undefined?"color":"duotone",pathCount,profile:selected.profile,provenance:{arch:process.arch,platform:process.platform,sharp:pixelToolchain.sharp,sharpVersions:pixelToolchain.sharpVersions,vips:pixelToolchain.vips,vtracerSha256:tool.sha256,vtracerSource:tool.source,vtracerVersion:tool.version},quality:roundedQuality(selected.quality),receiptVersion:1,representation:selected.representation,sourceSha256:raster.sourceSha256,svgSha256:sha256(svg),width:raster.width},svg}}function traceVariations(pixels,alphaCutoff){const sanitized=sanitizedTraceRgba(pixels,alphaCutoff);const output=[];if(containsVisiblePixel2(sanitized)){output.push({alphaCutoff,name:`alpha-${alphaCutoff}`,pixels:sanitized})}if(alphaCutoff>1&&(output.length===0||lowAlphaMassRatio(pixels,alphaCutoff)>=COMPARE_LOW_ALPHA_MASS_RATIO)){output.push({alphaCutoff:1,name:"alpha-1",pixels:sanitizedTraceRgba(pixels,1)})}if(output.length===0){throw new VectorizeError("invalid_input","Alpha sanitation removed every visible pixel.")}return output}function containsVisiblePixel2(pixels){for(let index=3;index<pixels.length;index+=4){if(pixels[index]>0)return true}return false}async function attemptCandidate(raster,variation,profile,sourcePath,tool,temporaryRoot,limits,deadline,errors){try{const raw=await traceRawSvg(tool,sourcePath,profile.args,temporaryRoot,limits.maxOutputBytes,deadline);deadline.assert(`${profile.name} trace`);const canonical=canonicalizeVTracerSvg(raw,raster.width,raster.height,limits.maxPaths);const bytes=Buffer.byteLength(canonical.svg);const pathCount=canonical.paths.length;if(bytes>limits.maxOutputBytes||pathCount>limits.maxPaths){throw new VectorizeError("output_limit",`${profile.name} exceeds vector output limits.`)}const quality=await scoreSvg(canonical.svg,raster,limits.maxDecodedPixels,deadline);return{alphaCutoff:variation.alphaCutoff,artworkPaths:canonical.paths,bytes,pathCount,profile:profile.name,quality,representation:"color-paths",svg:canonical.svg}}catch(error){if(error instanceof VectorizeError&&error.code==="timeout")throw error;errors.push(`${variation.name}/${profile.name}: ${error instanceof Error?error.message:String(error)}`);return}}async function attemptAlphaMask(raster,base,tool,temporaryRoot,limits,deadline,errors){try{const alphaPixels=alphaPlaneTraceRgba(raster.pixels);const sourcePath=join4(temporaryRoot,"alpha-plane.png");await writeFile2(sourcePath,await encodeTracePng(alphaPixels,raster.width,raster.height));const raw=await traceRawSvg(tool,sourcePath,alphaMaskArgs,temporaryRoot,limits.maxOutputBytes,deadline);const mask=canonicalizeVTracerSvg(raw,raster.width,raster.height,limits.maxPaths);const pathCount=base.artworkPaths.length+mask.paths.length;if(pathCount>limits.maxPaths){throw new VectorizeError("output_limit","Alpha-masked output exceeds the path limit.")}const svg=buildAlphaMaskedSvg(base.artworkPaths,mask.paths,raster.width,raster.height);const bytes=Buffer.byteLength(svg);if(bytes>limits.maxOutputBytes){throw new VectorizeError("output_limit","Alpha-masked output exceeds the byte limit.")}const quality=await scoreSvg(svg,raster,limits.maxDecodedPixels,deadline);return{alphaCutoff:base.alphaCutoff,artworkPaths:base.artworkPaths,bytes,maskPaths:mask.paths,pathCount,profile:base.profile,quality,representation:"alpha-mask",svg}}catch(error){if(error instanceof VectorizeError&&error.code==="timeout")throw error;errors.push(`alpha-mask: ${error instanceof Error?error.message:String(error)}`);return}}async function scoreSvg(svg,raster,maxDecodedPixels,deadline){const candidatePixels=await renderSvgRgba(svg,raster.scoreWidth,raster.scoreHeight,maxDecodedPixels);deadline.assert("quality measurement");const support=measureSupport(raster.scorePixels,candidatePixels,raster.scoreWidth,raster.scoreHeight,1);return{alphaRmse:normalizedAlphaRmse(raster.scorePixels,candidatePixels),colorRmse:normalizedPremultipliedRmse(raster.scorePixels,candidatePixels),outsideAlphaRatio:support.outsideAlphaRatio,sampleHeight:raster.scoreHeight,sampleWidth:raster.scoreWidth,supportRecall:support.supportRecall}}async function traceRawSvg(tool,sourcePath,args,temporaryRoot,maximumBytes,deadline){const{output}=await runBoundedPathOutputCommand((outputPath)=>[tool.path,"--input",sourcePath,"--output",outputPath,...args],deadline.remainingMs(),"trace_failed",{maxOutputBytes:maximumBytes,temporaryRoot});if(output.length===0){throw new VectorizeError("trace_failed","VTracer did not emit an SVG.")}return output}function passesFastQuality(quality){return passesQuality(quality)&&quality.colorRmse<=0.12&&quality.alphaRmse<=0.18&&quality.outsideAlphaRatio<=0.02&&quality.supportRecall>=0.97}function passesQuality(quality){return quality.colorRmse<=0.3&&quality.alphaRmse<=0.3&&quality.outsideAlphaRatio<=0.15&&quality.supportRecall>=0.8}function compareFidelity(left,right){return left.quality.colorRmse-right.quality.colorRmse||left.quality.alphaRmse-right.quality.alphaRmse||left.bytes-right.bytes||left.pathCount-right.pathCount||left.profile.localeCompare(right.profile)}function selectCandidate(candidates){const bestRmse=Math.min(...candidates.map(({quality})=>quality.colorRmse));const fidelityWindow=candidates.filter(({quality})=>quality.colorRmse<=bestRmse+0.005);return[...fidelityWindow].sort((left,right)=>left.bytes-right.bytes||left.pathCount-right.pathCount||left.quality.alphaRmse-right.quality.alphaRmse||left.profile.localeCompare(right.profile)||left.representation.localeCompare(right.representation))[0]}function roundedQuality(quality){const rounded=(value)=>Number(value.toFixed(8));return{alphaRmse:rounded(quality.alphaRmse),colorRmse:rounded(quality.colorRmse),outsideAlphaRatio:rounded(quality.outsideAlphaRatio),sampleHeight:quality.sampleHeight,sampleWidth:quality.sampleWidth,supportRecall:rounded(quality.supportRecall)}}function candidateSummary(candidate){return{alphaCutoff:candidate.alphaCutoff,bytes:candidate.bytes,pathCount:candidate.pathCount,profile:candidate.profile,quality:roundedQuality(candidate.quality),representation:candidate.representation}}async function writeSvgAtomically(path,svg){const outputPath=resolve3(path);await mkdir2(dirname3(outputPath),{recursive:true});const temporaryPath=`${outputPath}.${randomUUID2()}.tmp`;try{await writeFile2(temporaryPath,svg,{flag:"wx"});await rename2(temporaryPath,outputPath)}catch(error){await rm3(temporaryPath,{force:true});throw new VectorizeError("output_limit",`Could not atomically write ${outputPath}.`,{},{cause:error})}return outputPath}
|
|
10
10
|
export{vectorizeProfileNames,VectorizeError,vectorizeHardLimits,vectorizeDefaultLimits,resolveVectorizeLimits,VectorizeDeadline,parseHexColor,normalizedHexColor,configureVectorizeSharpConcurrency,loadRaster,encodeTracePng,withInheritedCommandFileDescriptors,forwardVectorizeWorkerTermination,VTRACER_VERSION,vtracerReleases,VECTORIZE_WORKER_PROTOCOL,MAX_VECTORIZE_REQUEST_BYTES,MAX_VECTORIZE_RESPONSE_BYTES,vectorizeImage,vectorizeImageInProcess};
|