auvrynt 1.0.2 → 1.0.5
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/LICENSE +21 -21
- package/README.md +366 -233
- package/addons/auvrynt_bridge/auvrynt_bridge.gd +500 -0
- package/addons/auvrynt_bridge/plugin.cfg +7 -0
- package/bin/auvrynt-launcher.cjs +77 -0
- package/dist/artifact-registry.js +1 -0
- package/dist/aseprite-tools.js +1 -0
- package/dist/auth/connection-registry.js +145 -0
- package/dist/auth/connection-status.js +176 -0
- package/dist/auth/oauth-provider.js +617 -0
- package/dist/auth/room-registry.js +131 -0
- package/dist/auth/session-registry.js +531 -0
- package/dist/background-lifecycle.js +1 -0
- package/dist/blender-client.js +1 -109
- package/dist/blender-tools.js +1 -930
- package/dist/browser-stability.js +1 -0
- package/dist/cli/commands/help-command.js +10 -0
- package/dist/cli/commands/init-command.js +190 -0
- package/dist/cli/commands/integration-commands.js +88 -0
- package/dist/cli/commands/serve-command.js +83 -0
- package/dist/cli/commands/setup-command.js +81 -0
- package/dist/cli/commands/status-commands.js +166 -0
- package/dist/cli/commands/tunnel-command.js +127 -0
- package/dist/cli/commands/user-config-commands.js +105 -0
- package/dist/cli/dashboard-actions.js +516 -0
- package/dist/cli/dashboard-native-actions.js +282 -0
- package/dist/cli/foreground-server.js +270 -0
- package/dist/cli/instance-control.js +21 -0
- package/dist/cli/instance-lock.js +123 -0
- package/dist/cli/integration-bootstrap.js +206 -0
- package/dist/cli/lifecycle-manager.js +493 -0
- package/dist/cli/runtime-support.js +74 -0
- package/dist/cli/serena-detected-default.js +27 -0
- package/dist/cli/tunnel-health-monitor.js +182 -0
- package/dist/cli.js +82 -739
- package/dist/config.js +1 -243
- package/dist/connection-registry.js +1 -73
- package/dist/connection-status.js +1 -139
- package/dist/control-retry.js +1 -0
- package/dist/crash-recovery.js +1 -0
- package/dist/db/client.js +16 -3
- package/dist/db/schema.js +30 -0
- package/dist/dotnet-tools.js +1 -223
- package/dist/git-worktrees.js +1 -134
- package/dist/git.js +1 -41
- package/dist/godot-animation-tilemap.js +1 -82
- package/dist/godot-csharp-build.js +1 -93
- package/dist/godot-csharp-project.js +1 -157
- package/dist/godot-csharp-runner.js +1 -146
- package/dist/godot-csharp-semantic.js +1 -161
- package/dist/godot-csharp-validate.js +1 -71
- package/dist/godot-dotnet-env.js +1 -169
- package/dist/godot-editor-bridge.js +1 -140
- package/dist/godot-gdscript.js +1 -824
- package/dist/godot-project-settings.js +1 -223
- package/dist/godot-runtime-testing.js +1 -249
- package/dist/godot-tools.js +1 -183
- package/dist/http-rate-limit.js +1 -0
- package/dist/http-server-hardening.js +1 -0
- package/dist/image-tools.js +1 -157
- package/dist/infrastructure/command-reference.js +32 -0
- package/dist/infrastructure/config.js +424 -0
- package/dist/infrastructure/context7-auth.js +55 -0
- package/dist/infrastructure/http-rate-limit.js +42 -0
- package/dist/infrastructure/http-server-hardening.js +23 -0
- package/dist/infrastructure/integration-queue.js +46 -0
- package/dist/infrastructure/logger.js +129 -0
- package/dist/infrastructure/mcp-event-store.js +103 -0
- package/dist/infrastructure/ngrok-auth-pool.js +208 -0
- package/dist/infrastructure/request-context.js +14 -0
- package/dist/infrastructure/shutdown.js +37 -0
- package/dist/infrastructure/sse-heartbeat.js +47 -0
- package/dist/infrastructure/tool-capabilities.js +67 -0
- package/dist/infrastructure/tool-result-budget.js +175 -0
- package/dist/infrastructure/user-config.js +167 -0
- package/dist/integration-discovery.js +1 -98
- package/dist/integration-queue.js +1 -0
- package/dist/integrations/agent-reach/agent-reach-tools.js +196 -0
- package/dist/integrations/agent-reach/native-channels.js +468 -0
- package/dist/integrations/aseprite/aseprite-analysis-tools.js +368 -0
- package/dist/integrations/aseprite/aseprite-editing-tools.js +647 -0
- package/dist/integrations/aseprite/aseprite-live-tools.js +297 -0
- package/dist/integrations/aseprite/aseprite-safety-tools.js +594 -0
- package/dist/integrations/aseprite/aseprite-specialized-tools.js +289 -0
- package/dist/integrations/aseprite/aseprite-tools.js +837 -0
- package/dist/integrations/blender/blender-catalog-mesh.js +593 -0
- package/dist/integrations/blender/blender-catalog-object.js +906 -0
- package/dist/integrations/blender/blender-catalog-scene.js +266 -0
- package/dist/integrations/blender/blender-catalog-select.js +581 -0
- package/dist/integrations/blender/blender-catalog-stubs.js +7512 -0
- package/dist/integrations/blender/blender-catalog.js +165 -0
- package/dist/integrations/blender/blender-client.js +105 -0
- package/dist/integrations/blender/blender-tools.js +2243 -0
- package/dist/integrations/context7/context7-tools.js +106 -0
- package/dist/integrations/dotnet/dotnet-tools.js +195 -0
- package/dist/integrations/godot/godot-animation-tilemap.js +74 -0
- package/dist/integrations/godot/godot-csharp-build.js +91 -0
- package/dist/integrations/godot/godot-csharp-project.js +155 -0
- package/dist/integrations/godot/godot-csharp-runner.js +118 -0
- package/dist/integrations/godot/godot-csharp-semantic.js +146 -0
- package/dist/integrations/godot/godot-csharp-validate.js +71 -0
- package/dist/integrations/godot/godot-dotnet-env.js +134 -0
- package/dist/integrations/godot/godot-editor-bridge.js +328 -0
- package/dist/integrations/godot/godot-gdscript.js +536 -0
- package/dist/integrations/godot/godot-project-settings.js +178 -0
- package/dist/integrations/godot/godot-runtime-testing.js +263 -0
- package/dist/integrations/godot/godot-tools.js +193 -0
- package/dist/integrations/images/image-tools.js +414 -0
- package/dist/integrations/images/png-codec.js +251 -0
- package/dist/integrations/images/view-image.js +99 -0
- package/dist/integrations/images/window-capture.js +148 -0
- package/dist/integrations/integration-discovery.js +185 -0
- package/dist/integrations/obsidian/obsidian-tools.js +205 -0
- package/dist/integrations/playwright/browser-session.js +516 -0
- package/dist/integrations/playwright/browser-stability.js +36 -0
- package/dist/integrations/playwright/playwright-runtime.js +105 -0
- package/dist/integrations/playwright/web-tools.js +504 -0
- package/dist/integrations/roblox/roblox-helpers.js +381 -0
- package/dist/integrations/roblox/roblox-studio-client.js +112 -0
- package/dist/integrations/roblox/roblox-tools.js +106 -0
- package/dist/integrations/serena/serena-manager.js +464 -0
- package/dist/integrations/serena/serena-tools.js +411 -0
- package/dist/logger.js +1 -76
- package/dist/mcp-event-store.js +1 -0
- package/dist/oauth-provider.js +1 -377
- package/dist/pi-tools.js +1 -81
- package/dist/playwright-runtime.js +1 -0
- package/dist/process/background-lifecycle.js +189 -0
- package/dist/process/control-retry.js +42 -0
- package/dist/process/crash-recovery.js +23 -0
- package/dist/process/processes.js +456 -0
- package/dist/processes.js +1 -218
- package/dist/request-context.js +1 -0
- package/dist/review-checkpoints.js +1 -142
- package/dist/room-registry.js +1 -0
- package/dist/roots.js +1 -53
- package/dist/search-discovery.js +1 -271
- package/dist/serena-manager.js +1 -443
- package/dist/serena-tools.js +1 -411
- package/dist/server/dashboard-page-styles.js +331 -0
- package/dist/server/dashboard-page.js +1509 -0
- package/dist/server/dashboard-routes.js +46 -0
- package/dist/server/dashboard.js +240 -0
- package/dist/server/http-helpers.js +30 -0
- package/dist/server/integration-profile-update.js +37 -0
- package/dist/server/mcp-policy.js +169 -0
- package/dist/server/mcp-server-factory.js +127 -0
- package/dist/server/mcp-tool-registrar.js +182 -0
- package/dist/server/openai-session-hint.js +14 -0
- package/dist/server/tool-groups.js +29 -0
- package/dist/server/tool-logging.js +33 -0
- package/dist/server/tool-registration-shared.js +59 -0
- package/dist/server/tools/aseprite-advanced-tools-registration.js +547 -0
- package/dist/server/tools/aseprite-tools-registration.js +213 -0
- package/dist/server/tools/blender-tools-registration.js +690 -0
- package/dist/server/tools/core-file-tools-registration.js +459 -0
- package/dist/server/tools/dotnet-godot-window-tools-registration.js +120 -0
- package/dist/server/tools/godot-csharp-tools-registration.js +252 -0
- package/dist/server/tools/godot-gdscript-tools-registration.js +189 -0
- package/dist/server/tools/process-project-tools-registration.js +155 -0
- package/dist/server/tools/roblox-tools-registration.js +514 -0
- package/dist/server/tools/web-image-tools-registration.js +326 -0
- package/dist/server/tools/workspace-lifecycle-tools-registration.js +130 -0
- package/dist/server/ui-assets.js +97 -0
- package/dist/server/workspace-analytics.js +92 -0
- package/dist/server.js +453 -2713
- package/dist/session-registry.js +1 -0
- package/dist/shutdown.js +1 -0
- package/dist/skills.js +1 -45
- package/dist/sse-heartbeat.js +1 -0
- package/dist/tool-capabilities.js +1 -0
- package/dist/tool-result-budget.js +1 -0
- package/dist/tools/pi-tools.js +81 -0
- package/dist/tunnels/cloudflare-tunnel.js +217 -0
- package/dist/tunnels/cloudflared-service.js +174 -0
- package/dist/tunnels/ngrok-tunnel.js +220 -0
- package/dist/tunnels/tunnel-manager.js +186 -0
- package/dist/tunnels/tunnel-types.js +1 -0
- package/dist/tunnels/tunnel-utils.js +62 -0
- package/dist/ui/.vite/manifest.json +9 -2487
- package/dist/ui/assets/heavy-payload-D42A9ceL.js +1 -0
- package/dist/ui/assets/lightweight-diff-Cv4g9Hdr.js +13 -0
- package/dist/ui/assets/review-payload-DKIg1cKF.js +1 -0
- package/dist/ui/assets/workspace-app-CeK3NhmB.css +1 -0
- package/dist/ui/assets/{workspace-app-C5QsE314.js → workspace-app-CkUXnpl4.js} +6 -6
- package/dist/ui/brand-theme.js +23 -0
- package/dist/ui/workspace-app.html +15 -15
- package/dist/user-config.js +1 -57
- package/dist/view-image.js +1 -104
- package/dist/web-tools.js +1 -200
- package/dist/window-capture.js +1 -74
- package/dist/workspace/artifact-registry.js +69 -0
- package/dist/workspace/git-worktrees.js +141 -0
- package/dist/workspace/git.js +42 -0
- package/dist/workspace/review-checkpoints.js +142 -0
- package/dist/workspace/roots.js +56 -0
- package/dist/workspace/search-discovery.js +264 -0
- package/dist/workspace/skills.js +45 -0
- package/dist/workspace/workspace-store.js +219 -0
- package/dist/workspace/workspaces.js +314 -0
- package/dist/workspace-store.js +1 -125
- package/dist/workspaces.js +1 -226
- package/docs/CREATOR_STORE_FREE_ASSETS.md +162 -0
- package/docs/DEVELOPMENT_TOOLS.md +136 -93
- package/docs/assets/auvrynt-icon.png +0 -0
- package/docs/assets/auvrynt-icon.svg +20 -0
- package/docs/chatgpt-coding-workflow.md +143 -143
- package/docs/configuration.md +156 -124
- package/docs/diagnostics.md +79 -0
- package/docs/gotchas.md +244 -202
- package/docs/multi-agent-and-long-projects.md +91 -0
- package/docs/security.md +138 -100
- package/docs/setup.md +171 -131
- package/docs/source-layout.md +60 -0
- package/extensions/auvrynt_bridge/auvrynt_bridge.lua +594 -0
- package/extensions/auvrynt_bridge/live_analysis.lua +181 -0
- package/extensions/auvrynt_bridge/live_animation.lua +134 -0
- package/extensions/auvrynt_bridge/live_commands.lua +24 -0
- package/extensions/auvrynt_bridge/live_common.lua +130 -0
- package/extensions/auvrynt_bridge/live_document.lua +78 -0
- package/extensions/auvrynt_bridge/live_drawing.lua +125 -0
- package/extensions/auvrynt_bridge/live_effects.lua +91 -0
- package/extensions/auvrynt_bridge/live_palette_template.lua +167 -0
- package/extensions/auvrynt_bridge/live_selection.lua +110 -0
- package/extensions/auvrynt_bridge/live_structure.lua +129 -0
- package/extensions/auvrynt_bridge/package.json +17 -0
- package/guides/ASEPRITE_AGENT_GUIDE.md +133 -0
- package/guides/BLENDER_AGENT_GUIDE.md +472 -0
- package/guides/GODOT_AGENT_GUIDE.md +298 -0
- package/guides/PROJECT_ROUTER.md +165 -0
- package/guides/ROBLOX_AGENT_GUIDE.md +75 -0
- package/guides/SOFTWARE_AGENT_GUIDE.md +104 -0
- package/guides/WEB_AGENT_GUIDE.md +287 -0
- package/package.json +78 -70
- package/scripts/dev-server.mjs +121 -120
- package/scripts/live-tunnel-smoke.mjs +538 -0
- package/scripts/repo-hygiene.mjs +74 -0
- package/scripts/restart-after-crash.mjs +49 -0
- package/scripts/run-tests.mjs +166 -0
- package/dist/ui/assets/abap-CLvhMVsD.js +0 -1
- package/dist/ui/assets/actionscript-3--17pq3dv.js +0 -1
- package/dist/ui/assets/ada-C5qYipkI.js +0 -1
- package/dist/ui/assets/andromeeda-vGVdxbeo.js +0 -1
- package/dist/ui/assets/angular-html-BJqQFYI6.js +0 -1
- package/dist/ui/assets/angular-ts-CMJECO0Y.js +0 -1
- package/dist/ui/assets/apache-U0d_L8uA.js +0 -1
- package/dist/ui/assets/apex-CGTLDQj6.js +0 -1
- package/dist/ui/assets/apl-DWeXpBBr.js +0 -1
- package/dist/ui/assets/applescript-CCn79oCD.js +0 -1
- package/dist/ui/assets/ara-4CJ0cIlV.js +0 -1
- package/dist/ui/assets/asciidoc-DE70LPWp.js +0 -1
- package/dist/ui/assets/asm-Cmm7eHzH.js +0 -1
- package/dist/ui/assets/astro-CLbttLOq.js +0 -1
- package/dist/ui/assets/aurora-x-CDeNXAV0.js +0 -1
- package/dist/ui/assets/awk-BWXHIvNe.js +0 -1
- package/dist/ui/assets/ayu-dark-DluEY0Gj.js +0 -1
- package/dist/ui/assets/ayu-light-C3h-C4tm.js +0 -1
- package/dist/ui/assets/ayu-mirage-Bqwy1Gya.js +0 -1
- package/dist/ui/assets/ballerina-B7ZEbQpA.js +0 -1
- package/dist/ui/assets/bat-Bo4NYOV-.js +0 -1
- package/dist/ui/assets/beancount-D-usSTwE.js +0 -1
- package/dist/ui/assets/berry-DKpUyyne.js +0 -1
- package/dist/ui/assets/bibtex-Ci_nEsc7.js +0 -1
- package/dist/ui/assets/bicep-CUHmPFLl.js +0 -1
- package/dist/ui/assets/bird2-C2hNVINV.js +0 -1
- package/dist/ui/assets/blade-BOzY0P2V.js +0 -1
- package/dist/ui/assets/bsl-BkkzgIyY.js +0 -1
- package/dist/ui/assets/c-BG-8uJvh.js +0 -1
- package/dist/ui/assets/c3-CnJL0r0V.js +0 -1
- package/dist/ui/assets/cadence-CQ2zXKGN.js +0 -1
- package/dist/ui/assets/cairo-DLTphjLi.js +0 -1
- package/dist/ui/assets/catppuccin-frappe-3VR1Za6u.js +0 -1
- package/dist/ui/assets/catppuccin-latte-DwIHMF0Q.js +0 -1
- package/dist/ui/assets/catppuccin-macchiato-DYnBP6_5.js +0 -1
- package/dist/ui/assets/catppuccin-mocha-DYhrFGRu.js +0 -1
- package/dist/ui/assets/clarity-SemFz856.js +0 -1
- package/dist/ui/assets/clojure-DqKBuwfJ.js +0 -1
- package/dist/ui/assets/cmake-Bj61d0ZC.js +0 -1
- package/dist/ui/assets/cobol-I_17IuGe.js +0 -1
- package/dist/ui/assets/codeowners-C8r90Shi.js +0 -1
- package/dist/ui/assets/codeql-oeQT6MSM.js +0 -1
- package/dist/ui/assets/coffee-C0B8hmok.js +0 -1
- package/dist/ui/assets/common-lisp-Cv5bFMCO.js +0 -1
- package/dist/ui/assets/coq-BrsZFFmf.js +0 -1
- package/dist/ui/assets/cpp-DDJXt6Vs.js +0 -1
- package/dist/ui/assets/crystal-Bexqace6.js +0 -1
- package/dist/ui/assets/csharp-Ct8U2NOr.js +0 -1
- package/dist/ui/assets/css-DSQhw_9R.js +0 -1
- package/dist/ui/assets/csv-Dx-8-gkx.js +0 -1
- package/dist/ui/assets/cue-CE9AQfxI.js +0 -1
- package/dist/ui/assets/cypher-ClKdZ_lG.js +0 -1
- package/dist/ui/assets/d-qD-0Kul2.js +0 -1
- package/dist/ui/assets/dark-plus-Cs2F2srj.js +0 -1
- package/dist/ui/assets/dart-DkHntEIa.js +0 -1
- package/dist/ui/assets/dax-BkyTk9wS.js +0 -1
- package/dist/ui/assets/desktop-Dlh5hvp9.js +0 -1
- package/dist/ui/assets/diff-woXpYk--.js +0 -1
- package/dist/ui/assets/docker-IyjqRm3v.js +0 -1
- package/dist/ui/assets/dotenv-_5a1GRtc.js +0 -1
- package/dist/ui/assets/dracula-BHWKrbxM.js +0 -1
- package/dist/ui/assets/dracula-soft-5eyTD99u.js +0 -1
- package/dist/ui/assets/dream-maker-DW3nJb8Q.js +0 -1
- package/dist/ui/assets/edge-Z8YYkHOr.js +0 -1
- package/dist/ui/assets/elixir-CG9uKtAF.js +0 -1
- package/dist/ui/assets/elm-GfWG2IfT.js +0 -1
- package/dist/ui/assets/emacs-lisp-C9PiwqqW.js +0 -1
- package/dist/ui/assets/erb-xe89e73J.js +0 -1
- package/dist/ui/assets/erlang-Cphh6RMH.js +0 -1
- package/dist/ui/assets/everforest-dark-sB-x3p7T.js +0 -1
- package/dist/ui/assets/everforest-light-Df2xbC6M.js +0 -1
- package/dist/ui/assets/fennel-DQxkIbk2.js +0 -1
- package/dist/ui/assets/fish-BJitypiv.js +0 -1
- package/dist/ui/assets/fluent-C03EYrpw.js +0 -1
- package/dist/ui/assets/fortran-fixed-form-DEKoE2YW.js +0 -1
- package/dist/ui/assets/fortran-free-form-CYNrtFtB.js +0 -1
- package/dist/ui/assets/fsharp-D13ZGOAj.js +0 -1
- package/dist/ui/assets/gdresource-C0sCabJj.js +0 -1
- package/dist/ui/assets/gdscript-Cp2uCuqX.js +0 -1
- package/dist/ui/assets/gdshader-CBce3t8t.js +0 -1
- package/dist/ui/assets/genie-CV2tkWYe.js +0 -1
- package/dist/ui/assets/gherkin-DExj1W_8.js +0 -1
- package/dist/ui/assets/git-commit-BSykSTBG.js +0 -1
- package/dist/ui/assets/git-rebase-CoTL_uSE.js +0 -1
- package/dist/ui/assets/github-dark-C-LZuMrd.js +0 -1
- package/dist/ui/assets/github-dark-default-DXG-b-1a.js +0 -1
- package/dist/ui/assets/github-dark-dimmed-Bx1FflLF.js +0 -1
- package/dist/ui/assets/github-dark-high-contrast-B_tTalzw.js +0 -1
- package/dist/ui/assets/github-light-EUqPIrTm.js +0 -1
- package/dist/ui/assets/github-light-default-BXViO-2h.js +0 -1
- package/dist/ui/assets/github-light-high-contrast-B68TUdTA.js +0 -1
- package/dist/ui/assets/gleam-CSRkHgEL.js +0 -1
- package/dist/ui/assets/glimmer-js-DAn4UCSu.js +0 -1
- package/dist/ui/assets/glimmer-ts-Tjyi6Gh5.js +0 -1
- package/dist/ui/assets/glsl-CtTDZoqR.js +0 -1
- package/dist/ui/assets/gn-ilITqXS6.js +0 -1
- package/dist/ui/assets/gnuplot-7GGW24-e.js +0 -1
- package/dist/ui/assets/go-BJwz_mda.js +0 -1
- package/dist/ui/assets/graphql-D5gcC_Jk.js +0 -1
- package/dist/ui/assets/groovy-CacY0gHj.js +0 -1
- package/dist/ui/assets/gruvbox-dark-hard-C820rvS2.js +0 -1
- package/dist/ui/assets/gruvbox-dark-medium-BPjhmG05.js +0 -1
- package/dist/ui/assets/gruvbox-dark-soft-MrdJrrXF.js +0 -1
- package/dist/ui/assets/gruvbox-light-hard-BC_s9l72.js +0 -1
- package/dist/ui/assets/gruvbox-light-medium-BAWPOn9u.js +0 -1
- package/dist/ui/assets/gruvbox-light-soft-BSMLrYjP.js +0 -1
- package/dist/ui/assets/hack-BJXY27dt.js +0 -1
- package/dist/ui/assets/haml-BLfFoZf7.js +0 -1
- package/dist/ui/assets/handlebars-Dw6CWkvN.js +0 -1
- package/dist/ui/assets/haskell-D8IpX4py.js +0 -1
- package/dist/ui/assets/haxe-OTjmBuCE.js +0 -1
- package/dist/ui/assets/hcl-Dh228itO.js +0 -1
- package/dist/ui/assets/heavy-payload-Dcg0-F-S.js +0 -4
- package/dist/ui/assets/hjson-CxZEssPk.js +0 -1
- package/dist/ui/assets/hlsl-Cvrh5tZx.js +0 -1
- package/dist/ui/assets/horizon-CE9ld1lL.js +0 -1
- package/dist/ui/assets/horizon-bright-Br1oVSNq.js +0 -1
- package/dist/ui/assets/houston-CsvMBhTu.js +0 -1
- package/dist/ui/assets/html-CAcAWvoN.js +0 -1
- package/dist/ui/assets/html-derivative-8aSW2E_7.js +0 -1
- package/dist/ui/assets/http-B2YR_3V1.js +0 -1
- package/dist/ui/assets/hurl-BnDJ9Zhf.js +0 -1
- package/dist/ui/assets/hxml-B0Qn7Nwc.js +0 -1
- package/dist/ui/assets/hy-CZbG8q4J.js +0 -1
- package/dist/ui/assets/imba-DsUTQ-LC.js +0 -1
- package/dist/ui/assets/ini-B5eOa1yu.js +0 -1
- package/dist/ui/assets/java-Doxi4W8u.js +0 -1
- package/dist/ui/assets/javascript-BbRdEqti.js +0 -1
- package/dist/ui/assets/jinja-CS3Tpv_Q.js +0 -1
- package/dist/ui/assets/jison-BkPGYVHR.js +0 -1
- package/dist/ui/assets/json-BMt1m3m8.js +0 -1
- package/dist/ui/assets/json5-BR5RXkoi.js +0 -1
- package/dist/ui/assets/jsonc-CYpm1nAK.js +0 -1
- package/dist/ui/assets/jsonl-CmCQp5Yx.js +0 -1
- package/dist/ui/assets/jsonnet-CJTPZ8u_.js +0 -1
- package/dist/ui/assets/jssm-DXw9l8Rf.js +0 -1
- package/dist/ui/assets/jsx-CQKrJXXY.js +0 -1
- package/dist/ui/assets/julia-BuN7i2du.js +0 -1
- package/dist/ui/assets/just-BswEPFPu.js +0 -1
- package/dist/ui/assets/kanagawa-dragon-CXtmUGW6.js +0 -1
- package/dist/ui/assets/kanagawa-lotus-BN08jTvb.js +0 -1
- package/dist/ui/assets/kanagawa-wave-CTweb8Dz.js +0 -1
- package/dist/ui/assets/kdl-CsD5j6eV.js +0 -1
- package/dist/ui/assets/kotlin-DhhofPvG.js +0 -1
- package/dist/ui/assets/kusto-BUv0MjJC.js +0 -1
- package/dist/ui/assets/laserwave-C_8bwKvT.js +0 -1
- package/dist/ui/assets/latex-DU2VyFfA.js +0 -1
- package/dist/ui/assets/lean-CewbzKMR.js +0 -1
- package/dist/ui/assets/less-DVTAwKKz.js +0 -1
- package/dist/ui/assets/light-plus-DVQuIRkW.js +0 -1
- package/dist/ui/assets/liquid-Cmz1L3dq.js +0 -1
- package/dist/ui/assets/llvm-Cm23YOpf.js +0 -1
- package/dist/ui/assets/log-BNLmms1o.js +0 -1
- package/dist/ui/assets/logo-Cluzi2Zq.js +0 -1
- package/dist/ui/assets/lua-CfVXhkaJ.js +0 -1
- package/dist/ui/assets/luau-CNKltnaQ.js +0 -1
- package/dist/ui/assets/make-Dixweg8N.js +0 -1
- package/dist/ui/assets/markdown-BYOwaDjH.js +0 -1
- package/dist/ui/assets/marko-C5bfqGiA.js +0 -1
- package/dist/ui/assets/material-theme-Bm3Qr25_.js +0 -1
- package/dist/ui/assets/material-theme-darker-2IIEA8gg.js +0 -1
- package/dist/ui/assets/material-theme-lighter-uhdI0v04.js +0 -1
- package/dist/ui/assets/material-theme-ocean-CHQ94UKr.js +0 -1
- package/dist/ui/assets/material-theme-palenight-B5W6OYN7.js +0 -1
- package/dist/ui/assets/matlab-D7qyCx1q.js +0 -1
- package/dist/ui/assets/mdc-CVBXYfyq.js +0 -1
- package/dist/ui/assets/mdx-DQZ5AkYe.js +0 -1
- package/dist/ui/assets/mermaid-Bk4SNUv9.js +0 -1
- package/dist/ui/assets/min-dark-BSWPekZh.js +0 -1
- package/dist/ui/assets/min-light-DDpmG2fV.js +0 -1
- package/dist/ui/assets/mipsasm-BMqwQI7S.js +0 -1
- package/dist/ui/assets/mojo-BgCJLMeH.js +0 -1
- package/dist/ui/assets/monokai-CdkpiU2Y.js +0 -1
- package/dist/ui/assets/moonbit-CaWjb8XO.js +0 -1
- package/dist/ui/assets/move-B1IS1UjX.js +0 -1
- package/dist/ui/assets/narrat-_X_XdTYD.js +0 -1
- package/dist/ui/assets/nextflow-Bbiyy34d.js +0 -1
- package/dist/ui/assets/nextflow-groovy-Dc_ddanL.js +0 -1
- package/dist/ui/assets/nginx-CzJafxLa.js +0 -1
- package/dist/ui/assets/night-owl-DhmEMT88.js +0 -1
- package/dist/ui/assets/night-owl-light-eJ-hLW7d.js +0 -1
- package/dist/ui/assets/nim-DABKtHtM.js +0 -1
- package/dist/ui/assets/nix-IvuFDN5E.js +0 -1
- package/dist/ui/assets/nord-Cb4Vim4T.js +0 -1
- package/dist/ui/assets/nushell-DcLAeLz5.js +0 -1
- package/dist/ui/assets/objective-c-D1A_Heim.js +0 -1
- package/dist/ui/assets/objective-cpp-BsSzOQcm.js +0 -1
- package/dist/ui/assets/ocaml-O90oeIOV.js +0 -1
- package/dist/ui/assets/odin-B1RWQWA5.js +0 -1
- package/dist/ui/assets/one-dark-pro-CLwyXe_n.js +0 -1
- package/dist/ui/assets/one-light-D7Lr4KcI.js +0 -1
- package/dist/ui/assets/openscad-BUDT5pXO.js +0 -1
- package/dist/ui/assets/pascal-4ZHwLPI5.js +0 -1
- package/dist/ui/assets/perl-CBh7L3lZ.js +0 -1
- package/dist/ui/assets/php-DFbI9q5p.js +0 -1
- package/dist/ui/assets/pierre-dark-sU4Zdns8.js +0 -1
- package/dist/ui/assets/pierre-dark-soft-HXgun5vs.js +0 -1
- package/dist/ui/assets/pierre-light-DWBk51d8.js +0 -1
- package/dist/ui/assets/pierre-light-soft-CodEzPxf.js +0 -1
- package/dist/ui/assets/pkl-ot-7Btpt.js +0 -1
- package/dist/ui/assets/plastic-DQwYfKfQ.js +0 -1
- package/dist/ui/assets/plsql-DGHpHOYJ.js +0 -1
- package/dist/ui/assets/po-BiJDBrnU.js +0 -1
- package/dist/ui/assets/poimandres-DRFjx7u4.js +0 -1
- package/dist/ui/assets/polar-C7UOKdEL.js +0 -1
- package/dist/ui/assets/postcss-BXeXVLqQ.js +0 -1
- package/dist/ui/assets/powerquery-DNMTfnFr.js +0 -1
- package/dist/ui/assets/powershell-DshXNtvi.js +0 -1
- package/dist/ui/assets/prisma-BsRQq5mF.js +0 -1
- package/dist/ui/assets/prolog-iXnhIJG7.js +0 -1
- package/dist/ui/assets/proto-DB4EqR-F.js +0 -1
- package/dist/ui/assets/pug-C_PxC7gO.js +0 -1
- package/dist/ui/assets/puppet-CDv2pdJW.js +0 -1
- package/dist/ui/assets/purescript-9MfHhQsQ.js +0 -1
- package/dist/ui/assets/python-gzcpVVnB.js +0 -1
- package/dist/ui/assets/qml-DtngoFFK.js +0 -1
- package/dist/ui/assets/qmldir-DCQb3MpD.js +0 -1
- package/dist/ui/assets/qss-Fe1Jh2GI.js +0 -1
- package/dist/ui/assets/r-DohkcevW.js +0 -1
- package/dist/ui/assets/racket-DcIDlBhZ.js +0 -1
- package/dist/ui/assets/raku-B3gFvitq.js +0 -1
- package/dist/ui/assets/razor-C4t8J0_E.js +0 -1
- package/dist/ui/assets/red-CJ3rzSJv.js +0 -1
- package/dist/ui/assets/reg-CRGYupPL.js +0 -1
- package/dist/ui/assets/regexp-OGbTbjgY.js +0 -1
- package/dist/ui/assets/rel-BtDbiS_P.js +0 -1
- package/dist/ui/assets/review-payload-C5Qz60Kq.js +0 -1
- package/dist/ui/assets/riscv-Ckw8ddFX.js +0 -1
- package/dist/ui/assets/ron-VUp2lXgN.js +0 -1
- package/dist/ui/assets/rose-pine-BthvhNj6.js +0 -1
- package/dist/ui/assets/rose-pine-dawn-Dg85fqjY.js +0 -1
- package/dist/ui/assets/rose-pine-moon-hon4tzzS.js +0 -1
- package/dist/ui/assets/rosmsg-CAekHB0j.js +0 -1
- package/dist/ui/assets/rst-CfBbbcvC.js +0 -1
- package/dist/ui/assets/ruby-Bw0mpxHY.js +0 -1
- package/dist/ui/assets/rust-Cfkwpbl8.js +0 -1
- package/dist/ui/assets/sas-Cs3s9JOf.js +0 -1
- package/dist/ui/assets/sass-DXrisJhu.js +0 -1
- package/dist/ui/assets/scala-DKOlJaKm.js +0 -1
- package/dist/ui/assets/scheme-DQCgrYNe.js +0 -1
- package/dist/ui/assets/scss-sRxNJS54.js +0 -1
- package/dist/ui/assets/sdbl-bTVj8UrX.js +0 -1
- package/dist/ui/assets/shaderlab-TOUzSsQk.js +0 -1
- package/dist/ui/assets/shellscript-B0X6wh0R.js +0 -1
- package/dist/ui/assets/shellsession-D2FdOioR.js +0 -1
- package/dist/ui/assets/slack-dark-DnToyrRv.js +0 -1
- package/dist/ui/assets/slack-ochin-B2OO5cIa.js +0 -1
- package/dist/ui/assets/smalltalk-B16xEiuN.js +0 -1
- package/dist/ui/assets/snazzy-light-4G7pJPwS.js +0 -1
- package/dist/ui/assets/solarized-dark-DV17i1UV.js +0 -1
- package/dist/ui/assets/solarized-light-DSh2HLQt.js +0 -1
- package/dist/ui/assets/solidity-CKzVLygQ.js +0 -1
- package/dist/ui/assets/soy-s6g9OjJ1.js +0 -1
- package/dist/ui/assets/sparql-D_iOobhT.js +0 -1
- package/dist/ui/assets/splunk-BC2Px7Mm.js +0 -1
- package/dist/ui/assets/sql-DVTnh5V-.js +0 -1
- package/dist/ui/assets/ssh-config-BgfXC-Er.js +0 -1
- package/dist/ui/assets/stata-DFIDiXF9.js +0 -1
- package/dist/ui/assets/stylus-B6D30XZt.js +0 -1
- package/dist/ui/assets/surrealql-DVfrsLyg.js +0 -1
- package/dist/ui/assets/svelte-D5GsxESq.js +0 -1
- package/dist/ui/assets/swift-DonLKvLd.js +0 -1
- package/dist/ui/assets/synthwave-84-nFMaYfgc.js +0 -1
- package/dist/ui/assets/system-verilog-DJ5XKQeo.js +0 -1
- package/dist/ui/assets/systemd-BxMlprV5.js +0 -1
- package/dist/ui/assets/talonscript-CohzipZa.js +0 -1
- package/dist/ui/assets/tasl-DMoTqEGO.js +0 -1
- package/dist/ui/assets/tcl-CZd0xW_V.js +0 -1
- package/dist/ui/assets/templ-DI-Zyqzd.js +0 -1
- package/dist/ui/assets/terraform-DswuEJGm.js +0 -1
- package/dist/ui/assets/tex-WHHmlbJI.js +0 -1
- package/dist/ui/assets/tokyo-night-oM2G3aXe.js +0 -1
- package/dist/ui/assets/toml-CcmNWLt0.js +0 -1
- package/dist/ui/assets/ts-tags-Dwpo2RFJ.js +0 -1
- package/dist/ui/assets/tsv-sltzmVWM.js +0 -1
- package/dist/ui/assets/tsx-ph_idaAW.js +0 -1
- package/dist/ui/assets/turtle-ByJddavk.js +0 -1
- package/dist/ui/assets/twig-YO17jFjR.js +0 -1
- package/dist/ui/assets/typescript-CbAxjhkl.js +0 -1
- package/dist/ui/assets/typespec-B88KGewJ.js +0 -1
- package/dist/ui/assets/typst-DI99ib-x.js +0 -1
- package/dist/ui/assets/useFileDiffInstance-B2DLX3Vg.js +0 -244
- package/dist/ui/assets/v-DETTlOr0.js +0 -1
- package/dist/ui/assets/vala-zf12oZj6.js +0 -1
- package/dist/ui/assets/vb-Djn5o6TS.js +0 -1
- package/dist/ui/assets/verilog-CiiDBU1e.js +0 -1
- package/dist/ui/assets/vesper-D5bVUKB1.js +0 -1
- package/dist/ui/assets/vhdl-BroJfC0k.js +0 -1
- package/dist/ui/assets/viml-DvXPmvsu.js +0 -1
- package/dist/ui/assets/vitesse-black-fwtXNY1n.js +0 -1
- package/dist/ui/assets/vitesse-dark-BZCL-v6S.js +0 -1
- package/dist/ui/assets/vitesse-light-VbXTXTou.js +0 -1
- package/dist/ui/assets/vue-CraHL-Ps.js +0 -1
- package/dist/ui/assets/vue-html-e9tyZNtS.js +0 -1
- package/dist/ui/assets/vue-vine-CofmxaUb.js +0 -1
- package/dist/ui/assets/vyper-CgoNMtux.js +0 -1
- package/dist/ui/assets/wasm-BnjxR4X6.js +0 -1
- package/dist/ui/assets/wasm-ByWQv1Qj.js +0 -1
- package/dist/ui/assets/wenyan-C8pVoKbM.js +0 -1
- package/dist/ui/assets/wgsl-BsKzXJz4.js +0 -1
- package/dist/ui/assets/wikitext-ClFFjSW2.js +0 -1
- package/dist/ui/assets/wit-DdvCle-K.js +0 -1
- package/dist/ui/assets/wolfram-DLL8P-h_.js +0 -1
- package/dist/ui/assets/workspace-app-8--oTbCd.css +0 -1
- package/dist/ui/assets/xml-BWSdPZHm.js +0 -1
- package/dist/ui/assets/xsl-BtCt7Hx8.js +0 -1
- package/dist/ui/assets/yaml-BZiTw7cQ.js +0 -1
- package/dist/ui/assets/zenscript-BnlCZFoB.js +0 -1
- package/dist/ui/assets/zig-CMLA9XwU.js +0 -1
- package/docs/assets/auvrynt-logo-light.png +0 -0
- package/docs/assets/auvrynt-screenshot.png +0 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import { access, mkdir, readFile, stat, unlink, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, extname, join, relative } from "node:path";
|
|
3
|
+
import { inlineImageOrNotice } from "../../tool-result-budget.js";
|
|
4
|
+
import { decodePng, encodePng } from "./png-codec.js";
|
|
5
|
+
const MAX_IMAGE_SIZE_BYTES = 20 * 1024 * 1024;
|
|
6
|
+
const MAX_COMPARE_PIXELS = 16_777_216;
|
|
7
|
+
const MAX_SPRITE_FRAMES = 4_096;
|
|
8
|
+
export function parseImageDimensions(buffer) {
|
|
9
|
+
if (buffer.length < 10)
|
|
10
|
+
throw new Error("Image file buffer is too small to identify.");
|
|
11
|
+
if (isPng(buffer)) {
|
|
12
|
+
if (buffer.length < 24)
|
|
13
|
+
throw new Error("PNG file is missing its IHDR dimensions.");
|
|
14
|
+
return validateDimensions(buffer.readUInt32BE(16), buffer.readUInt32BE(20));
|
|
15
|
+
}
|
|
16
|
+
if (buffer.toString("ascii", 0, 3) === "GIF") {
|
|
17
|
+
return validateDimensions(buffer.readUInt16LE(6), buffer.readUInt16LE(8));
|
|
18
|
+
}
|
|
19
|
+
if (buffer.length >= 30
|
|
20
|
+
&& buffer.toString("ascii", 0, 4) === "RIFF"
|
|
21
|
+
&& buffer.toString("ascii", 8, 12) === "WEBP") {
|
|
22
|
+
const chunkType = buffer.toString("ascii", 12, 16);
|
|
23
|
+
if (chunkType === "VP8 ") {
|
|
24
|
+
return validateDimensions(buffer.readUInt16LE(26) & 0x3fff, buffer.readUInt16LE(28) & 0x3fff);
|
|
25
|
+
}
|
|
26
|
+
if (chunkType === "VP8L" && buffer[20] === 0x2f) {
|
|
27
|
+
const b1 = buffer[21];
|
|
28
|
+
const b2 = buffer[22];
|
|
29
|
+
const b3 = buffer[23];
|
|
30
|
+
const b4 = buffer[24];
|
|
31
|
+
return validateDimensions(1 + (((b2 & 0x3f) << 8) | b1), 1 + (((b4 & 0x0f) << 10) | (b3 << 2) | ((b2 & 0xc0) >> 6)));
|
|
32
|
+
}
|
|
33
|
+
if (chunkType === "VP8X") {
|
|
34
|
+
return validateDimensions(1 + readUInt24LE(buffer, 24), 1 + readUInt24LE(buffer, 27));
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Unsupported WebP chunk type ${JSON.stringify(chunkType)}.`);
|
|
37
|
+
}
|
|
38
|
+
if (buffer[0] === 0xff && buffer[1] === 0xd8) {
|
|
39
|
+
return parseJpegDimensions(buffer);
|
|
40
|
+
}
|
|
41
|
+
throw new Error("Unsupported or malformed image format. Supported formats: PNG, JPEG, GIF, and WebP.");
|
|
42
|
+
}
|
|
43
|
+
export async function inspectImage(registry, input) {
|
|
44
|
+
const workspace = registry.getWorkspace(input.workspaceId);
|
|
45
|
+
const absolutePath = registry.resolvePath(workspace, input.path);
|
|
46
|
+
const { buffer, fileSize } = await readBoundedImage(absolutePath);
|
|
47
|
+
const ext = extname(input.path).toLowerCase();
|
|
48
|
+
const dimensions = parseImageDimensions(buffer);
|
|
49
|
+
const mimeType = mimeTypeForExtension(ext);
|
|
50
|
+
const info = {
|
|
51
|
+
format: ext.slice(1),
|
|
52
|
+
mimeType,
|
|
53
|
+
width: dimensions.width,
|
|
54
|
+
height: dimensions.height,
|
|
55
|
+
fileSize,
|
|
56
|
+
hasAlpha: detectAlphaCapability(buffer, ext),
|
|
57
|
+
pixelArtStatus: dimensions.width <= 256 && dimensions.height <= 256 ? "likely" : "unlikely",
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
content: [{ type: "text", text: JSON.stringify(info, null, 2) }],
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
export async function compareImages(registry, input) {
|
|
64
|
+
const workspace = registry.getWorkspace(input.workspaceId);
|
|
65
|
+
const refPath = registry.resolvePath(workspace, input.referencePath);
|
|
66
|
+
const candPath = registry.resolvePath(workspace, input.candidatePath);
|
|
67
|
+
const threshold = validateThreshold(input.threshold);
|
|
68
|
+
const [referenceFile, candidateFile] = await Promise.all([
|
|
69
|
+
readBoundedImage(refPath),
|
|
70
|
+
readBoundedImage(candPath),
|
|
71
|
+
]);
|
|
72
|
+
const reference = decodePng(referenceFile.buffer);
|
|
73
|
+
const candidate = decodePng(candidateFile.buffer);
|
|
74
|
+
const dimensionsMatch = reference.width === candidate.width && reference.height === candidate.height;
|
|
75
|
+
const isExactByteMatch = referenceFile.buffer.equals(candidateFile.buffer);
|
|
76
|
+
const comparison = compareDecodedImages(reference, candidate, threshold, input.ignoreTransparentPixels ?? false);
|
|
77
|
+
let diffPath;
|
|
78
|
+
let diffBuffer;
|
|
79
|
+
if (input.diffOutputPath) {
|
|
80
|
+
if (extname(input.diffOutputPath).toLowerCase() !== ".png") {
|
|
81
|
+
throw new Error("Image diff output must use a .png extension.");
|
|
82
|
+
}
|
|
83
|
+
const absoluteDiffPath = registry.resolveArtifactPath(workspace, input.diffOutputPath, "images");
|
|
84
|
+
await mkdir(dirname(absoluteDiffPath), { recursive: true });
|
|
85
|
+
diffBuffer = encodePng(comparison.diff);
|
|
86
|
+
await writeFile(absoluteDiffPath, diffBuffer);
|
|
87
|
+
diffPath = relative(workspace.root, absoluteDiffPath).replace(/\\/g, "/");
|
|
88
|
+
}
|
|
89
|
+
const resultData = {
|
|
90
|
+
referenceDimensions: { width: reference.width, height: reference.height },
|
|
91
|
+
candidateDimensions: { width: candidate.width, height: candidate.height },
|
|
92
|
+
dimensionsMatch,
|
|
93
|
+
isExactByteMatch,
|
|
94
|
+
isExactPixelMatch: dimensionsMatch && comparison.exactMatchingPixels === comparison.comparedPixels,
|
|
95
|
+
threshold,
|
|
96
|
+
ignoredTransparentPixels: comparison.ignoredTransparentPixels,
|
|
97
|
+
comparedPixels: comparison.comparedPixels,
|
|
98
|
+
exactMatchingPixels: comparison.exactMatchingPixels,
|
|
99
|
+
matchingPixels: comparison.matchingPixels,
|
|
100
|
+
changedPixels: comparison.changedPixels,
|
|
101
|
+
exactMatchPercentage: comparison.exactMatchPercentage,
|
|
102
|
+
toleranceMatchPercentage: comparison.toleranceMatchPercentage,
|
|
103
|
+
similarityPercentage: comparison.similarityPercentage,
|
|
104
|
+
maxChannelDifference: comparison.maxChannelDifference,
|
|
105
|
+
meanRgbaDifference: comparison.meanRgbaDifference,
|
|
106
|
+
diffOutputPath: diffPath,
|
|
107
|
+
};
|
|
108
|
+
const responseContent = [
|
|
109
|
+
{ type: "text", text: JSON.stringify(resultData, null, 2) },
|
|
110
|
+
];
|
|
111
|
+
if (diffBuffer) {
|
|
112
|
+
responseContent.unshift(...inlineImageOrNotice(diffBuffer, `Image diff ${diffPath ?? "output"}`, "image/png"));
|
|
113
|
+
}
|
|
114
|
+
return { content: responseContent };
|
|
115
|
+
}
|
|
116
|
+
export async function inspectSprite(registry, input) {
|
|
117
|
+
const workspace = registry.getWorkspace(input.workspaceId);
|
|
118
|
+
const absolutePath = registry.resolvePath(workspace, input.path);
|
|
119
|
+
const { buffer } = await readBoundedImage(absolutePath);
|
|
120
|
+
const dimensions = parseImageDimensions(buffer);
|
|
121
|
+
const cellWidth = validateCellDimension(input.expectedCellWidth ?? 32, "width");
|
|
122
|
+
const cellHeight = validateCellDimension(input.expectedCellHeight ?? 32, "height");
|
|
123
|
+
const possibleColumns = Math.floor(dimensions.width / cellWidth);
|
|
124
|
+
const possibleRows = Math.floor(dimensions.height / cellHeight);
|
|
125
|
+
const dividesEvenly = dimensions.width % cellWidth === 0 && dimensions.height % cellHeight === 0;
|
|
126
|
+
return {
|
|
127
|
+
content: [{
|
|
128
|
+
type: "text",
|
|
129
|
+
text: JSON.stringify({
|
|
130
|
+
dimensions,
|
|
131
|
+
cellWidth,
|
|
132
|
+
cellHeight,
|
|
133
|
+
possibleColumns,
|
|
134
|
+
possibleRows,
|
|
135
|
+
dividesEvenly,
|
|
136
|
+
pixelArtStatus: dimensions.width <= 1024 && dimensions.height <= 1024 ? "likely" : "unknown",
|
|
137
|
+
}, null, 2),
|
|
138
|
+
}],
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
export async function splitSpriteSheet(registry, input) {
|
|
142
|
+
const workspace = registry.getWorkspace(input.workspaceId);
|
|
143
|
+
const absolutePath = registry.resolvePath(workspace, input.path);
|
|
144
|
+
const outputDirectory = registry.resolveArtifactPath(workspace, input.outputDirectory, "images");
|
|
145
|
+
const columns = validateGridDimension(input.columns, "columns");
|
|
146
|
+
const rows = validateGridDimension(input.rows, "rows");
|
|
147
|
+
if (columns * rows > MAX_SPRITE_FRAMES) {
|
|
148
|
+
throw new Error(`Sprite grid exceeds the ${MAX_SPRITE_FRAMES}-frame safety limit.`);
|
|
149
|
+
}
|
|
150
|
+
const { buffer } = await readBoundedImage(absolutePath);
|
|
151
|
+
const source = decodePng(buffer);
|
|
152
|
+
if (source.width % columns !== 0 || source.height % rows !== 0) {
|
|
153
|
+
throw new Error(`Sprite dimensions ${source.width}x${source.height} are not evenly divisible by ${columns} columns and ${rows} rows.`);
|
|
154
|
+
}
|
|
155
|
+
const frameWidth = source.width / columns;
|
|
156
|
+
const frameHeight = source.height / rows;
|
|
157
|
+
const pattern = input.namingPattern ?? "frame_{col}_{row}.png";
|
|
158
|
+
const plannedFrames = [];
|
|
159
|
+
const plannedNames = new Set();
|
|
160
|
+
for (let row = 0; row < rows; row++) {
|
|
161
|
+
for (let column = 0; column < columns; column++) {
|
|
162
|
+
const fileName = frameFileName(pattern, column, row);
|
|
163
|
+
const normalizedName = fileName.toLowerCase();
|
|
164
|
+
if (plannedNames.has(normalizedName)) {
|
|
165
|
+
throw new Error(`Sprite naming pattern produces duplicate filename: ${fileName}`);
|
|
166
|
+
}
|
|
167
|
+
plannedNames.add(normalizedName);
|
|
168
|
+
const framePath = join(outputDirectory, fileName);
|
|
169
|
+
const frame = cropRgba(source, column * frameWidth, row * frameHeight, frameWidth, frameHeight);
|
|
170
|
+
plannedFrames.push({
|
|
171
|
+
path: framePath,
|
|
172
|
+
relativePath: relative(workspace.root, framePath).replace(/\\/g, "/"),
|
|
173
|
+
buffer: encodePng(frame),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
for (const frame of plannedFrames) {
|
|
178
|
+
try {
|
|
179
|
+
await access(frame.path);
|
|
180
|
+
throw new Error(`Refusing to overwrite existing sprite frame: ${frame.relativePath}`);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
if (error instanceof Error && error.message.startsWith("Refusing to overwrite"))
|
|
184
|
+
throw error;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
await mkdir(outputDirectory, { recursive: true });
|
|
188
|
+
const writtenPaths = [];
|
|
189
|
+
try {
|
|
190
|
+
for (const frame of plannedFrames) {
|
|
191
|
+
await writeFile(frame.path, frame.buffer, { flag: "wx" });
|
|
192
|
+
writtenPaths.push(frame.path);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
catch (error) {
|
|
196
|
+
await Promise.allSettled(writtenPaths.map((path) => unlink(path)));
|
|
197
|
+
throw error;
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
content: [{
|
|
201
|
+
type: "text",
|
|
202
|
+
text: `Split sprite sheet into ${plannedFrames.length} frames (${frameWidth}x${frameHeight} each):\n${plannedFrames.map((frame) => frame.relativePath).join("\n")}`,
|
|
203
|
+
}],
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
function compareDecodedImages(reference, candidate, threshold, ignoreTransparentPixels) {
|
|
207
|
+
const width = Math.max(reference.width, candidate.width);
|
|
208
|
+
const height = Math.max(reference.height, candidate.height);
|
|
209
|
+
if (width * height > MAX_COMPARE_PIXELS) {
|
|
210
|
+
throw new Error(`Comparison canvas exceeds the ${MAX_COMPARE_PIXELS.toLocaleString("en-US")}-pixel safety limit.`);
|
|
211
|
+
}
|
|
212
|
+
const diffData = Buffer.alloc(width * height * 4);
|
|
213
|
+
let comparedPixels = 0;
|
|
214
|
+
let ignoredTransparentPixels = 0;
|
|
215
|
+
let exactMatchingPixels = 0;
|
|
216
|
+
let matchingPixels = 0;
|
|
217
|
+
let changedPixels = 0;
|
|
218
|
+
let maximumDifference = 0;
|
|
219
|
+
let differenceSum = 0;
|
|
220
|
+
let similaritySum = 0;
|
|
221
|
+
for (let y = 0; y < height; y++) {
|
|
222
|
+
for (let x = 0; x < width; x++) {
|
|
223
|
+
const referenceContainsPixel = x < reference.width && y < reference.height;
|
|
224
|
+
const candidateContainsPixel = x < candidate.width && y < candidate.height;
|
|
225
|
+
if (!referenceContainsPixel && !candidateContainsPixel)
|
|
226
|
+
continue;
|
|
227
|
+
const ref = pixelAt(reference, x, y);
|
|
228
|
+
const cand = pixelAt(candidate, x, y);
|
|
229
|
+
if (ignoreTransparentPixels && ref[3] === 0 && cand[3] === 0) {
|
|
230
|
+
ignoredTransparentPixels++;
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
comparedPixels++;
|
|
234
|
+
const differences = [
|
|
235
|
+
Math.abs(ref[0] - cand[0]),
|
|
236
|
+
Math.abs(ref[1] - cand[1]),
|
|
237
|
+
Math.abs(ref[2] - cand[2]),
|
|
238
|
+
Math.abs(ref[3] - cand[3]),
|
|
239
|
+
];
|
|
240
|
+
const pixelMaximum = Math.max(...differences);
|
|
241
|
+
const pixelDifference = differences.reduce((sum, value) => sum + value, 0);
|
|
242
|
+
maximumDifference = Math.max(maximumDifference, pixelMaximum);
|
|
243
|
+
differenceSum += pixelDifference;
|
|
244
|
+
similaritySum += 1 - pixelDifference / (4 * 255);
|
|
245
|
+
const target = (y * width + x) * 4;
|
|
246
|
+
if (pixelMaximum === 0)
|
|
247
|
+
exactMatchingPixels++;
|
|
248
|
+
if (pixelMaximum <= threshold) {
|
|
249
|
+
matchingPixels++;
|
|
250
|
+
diffData[target] = differences[0];
|
|
251
|
+
diffData[target + 1] = differences[1];
|
|
252
|
+
diffData[target + 2] = differences[2];
|
|
253
|
+
diffData[target + 3] = pixelMaximum;
|
|
254
|
+
}
|
|
255
|
+
else {
|
|
256
|
+
changedPixels++;
|
|
257
|
+
diffData[target] = 255;
|
|
258
|
+
diffData[target + 1] = Math.min(255, differences[1]);
|
|
259
|
+
diffData[target + 2] = 255;
|
|
260
|
+
diffData[target + 3] = Math.max(96, pixelMaximum);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const denominator = Math.max(comparedPixels, 1);
|
|
265
|
+
return {
|
|
266
|
+
comparedPixels,
|
|
267
|
+
ignoredTransparentPixels,
|
|
268
|
+
exactMatchingPixels,
|
|
269
|
+
matchingPixels,
|
|
270
|
+
changedPixels,
|
|
271
|
+
exactMatchPercentage: Number(((exactMatchingPixels / denominator) * 100).toFixed(2)),
|
|
272
|
+
toleranceMatchPercentage: Number(((matchingPixels / denominator) * 100).toFixed(2)),
|
|
273
|
+
similarityPercentage: Number(((similaritySum / denominator) * 100).toFixed(2)),
|
|
274
|
+
maxChannelDifference: maximumDifference,
|
|
275
|
+
meanRgbaDifference: Number((differenceSum / (denominator * 4)).toFixed(4)),
|
|
276
|
+
diff: { width, height, data: diffData },
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
function cropRgba(source, startX, startY, width, height) {
|
|
280
|
+
const data = Buffer.alloc(width * height * 4);
|
|
281
|
+
const sourceRowBytes = source.width * 4;
|
|
282
|
+
const targetRowBytes = width * 4;
|
|
283
|
+
for (let row = 0; row < height; row++) {
|
|
284
|
+
const sourceOffset = (startY + row) * sourceRowBytes + startX * 4;
|
|
285
|
+
source.data.copy(data, row * targetRowBytes, sourceOffset, sourceOffset + targetRowBytes);
|
|
286
|
+
}
|
|
287
|
+
return { width, height, data };
|
|
288
|
+
}
|
|
289
|
+
function pixelAt(image, x, y) {
|
|
290
|
+
if (x >= image.width || y >= image.height)
|
|
291
|
+
return [0, 0, 0, 0];
|
|
292
|
+
const offset = (y * image.width + x) * 4;
|
|
293
|
+
return [
|
|
294
|
+
image.data[offset],
|
|
295
|
+
image.data[offset + 1],
|
|
296
|
+
image.data[offset + 2],
|
|
297
|
+
image.data[offset + 3],
|
|
298
|
+
];
|
|
299
|
+
}
|
|
300
|
+
async function readBoundedImage(path) {
|
|
301
|
+
const fileStats = await stat(path);
|
|
302
|
+
if (!fileStats.isFile())
|
|
303
|
+
throw new Error(`Image path is not a file: ${path}`);
|
|
304
|
+
if (fileStats.size > MAX_IMAGE_SIZE_BYTES) {
|
|
305
|
+
throw new Error(`Image file size exceeds the ${MAX_IMAGE_SIZE_BYTES / 1024 / 1024} MB safety limit.`);
|
|
306
|
+
}
|
|
307
|
+
return { buffer: await readFile(path), fileSize: fileStats.size };
|
|
308
|
+
}
|
|
309
|
+
function frameFileName(pattern, column, row) {
|
|
310
|
+
const fileName = pattern
|
|
311
|
+
.replaceAll("{col}", String(column))
|
|
312
|
+
.replaceAll("{row}", String(row));
|
|
313
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,199}\.png$/i.test(fileName)) {
|
|
314
|
+
throw new Error("Sprite namingPattern must produce a safe PNG filename, not a path.");
|
|
315
|
+
}
|
|
316
|
+
const stem = fileName.slice(0, -4).toUpperCase();
|
|
317
|
+
if (/^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])(?:\.|$)/.test(stem)) {
|
|
318
|
+
throw new Error(`Sprite frame filename is reserved on Windows: ${fileName}`);
|
|
319
|
+
}
|
|
320
|
+
return fileName;
|
|
321
|
+
}
|
|
322
|
+
function validateThreshold(value) {
|
|
323
|
+
const threshold = value ?? 0;
|
|
324
|
+
if (!Number.isFinite(threshold) || threshold < 0 || threshold > 255) {
|
|
325
|
+
throw new Error("Image comparison threshold must be between 0 and 255.");
|
|
326
|
+
}
|
|
327
|
+
return threshold;
|
|
328
|
+
}
|
|
329
|
+
function validateGridDimension(value, label) {
|
|
330
|
+
if (!Number.isInteger(value) || value < 1 || value > 256) {
|
|
331
|
+
throw new Error(`Sprite ${label} must be an integer between 1 and 256.`);
|
|
332
|
+
}
|
|
333
|
+
return value;
|
|
334
|
+
}
|
|
335
|
+
function validateCellDimension(value, label) {
|
|
336
|
+
if (!Number.isInteger(value) || value < 1 || value > 16_384) {
|
|
337
|
+
throw new Error(`Sprite cell ${label} must be an integer between 1 and 16384.`);
|
|
338
|
+
}
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
function validateDimensions(width, height) {
|
|
342
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) {
|
|
343
|
+
throw new Error(`Invalid image dimensions: ${width}x${height}.`);
|
|
344
|
+
}
|
|
345
|
+
return { width, height };
|
|
346
|
+
}
|
|
347
|
+
function mimeTypeForExtension(extension) {
|
|
348
|
+
const mimeTypes = {
|
|
349
|
+
".png": "image/png",
|
|
350
|
+
".jpg": "image/jpeg",
|
|
351
|
+
".jpeg": "image/jpeg",
|
|
352
|
+
".webp": "image/webp",
|
|
353
|
+
".gif": "image/gif",
|
|
354
|
+
};
|
|
355
|
+
const mimeType = mimeTypes[extension];
|
|
356
|
+
if (!mimeType)
|
|
357
|
+
throw new Error(`Unsupported image extension ${JSON.stringify(extension)}.`);
|
|
358
|
+
return mimeType;
|
|
359
|
+
}
|
|
360
|
+
function detectAlphaCapability(buffer, extension) {
|
|
361
|
+
if (extension === ".png" && buffer.length > 25)
|
|
362
|
+
return [4, 6].includes(buffer[25]);
|
|
363
|
+
if (extension === ".gif")
|
|
364
|
+
return true;
|
|
365
|
+
if (extension === ".webp")
|
|
366
|
+
return true;
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
369
|
+
function isPng(buffer) {
|
|
370
|
+
return buffer.length >= 8
|
|
371
|
+
&& buffer[0] === 0x89
|
|
372
|
+
&& buffer[1] === 0x50
|
|
373
|
+
&& buffer[2] === 0x4e
|
|
374
|
+
&& buffer[3] === 0x47
|
|
375
|
+
&& buffer[4] === 0x0d
|
|
376
|
+
&& buffer[5] === 0x0a
|
|
377
|
+
&& buffer[6] === 0x1a
|
|
378
|
+
&& buffer[7] === 0x0a;
|
|
379
|
+
}
|
|
380
|
+
function readUInt24LE(buffer, offset) {
|
|
381
|
+
return buffer[offset] | (buffer[offset + 1] << 8) | (buffer[offset + 2] << 16);
|
|
382
|
+
}
|
|
383
|
+
function parseJpegDimensions(buffer) {
|
|
384
|
+
let offset = 2;
|
|
385
|
+
while (offset + 3 < buffer.length) {
|
|
386
|
+
if (buffer[offset] !== 0xff) {
|
|
387
|
+
offset++;
|
|
388
|
+
continue;
|
|
389
|
+
}
|
|
390
|
+
while (buffer[offset] === 0xff)
|
|
391
|
+
offset++;
|
|
392
|
+
const marker = buffer[offset++];
|
|
393
|
+
if (marker === 0xd8 || marker === 0xd9 || marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (offset + 2 > buffer.length)
|
|
397
|
+
break;
|
|
398
|
+
const segmentLength = buffer.readUInt16BE(offset);
|
|
399
|
+
if (segmentLength < 2 || offset + segmentLength > buffer.length) {
|
|
400
|
+
throw new Error("JPEG contains an invalid segment length.");
|
|
401
|
+
}
|
|
402
|
+
const isStartOfFrame = [
|
|
403
|
+
0xc0, 0xc1, 0xc2, 0xc3, 0xc5, 0xc6, 0xc7,
|
|
404
|
+
0xc9, 0xca, 0xcb, 0xcd, 0xce, 0xcf,
|
|
405
|
+
].includes(marker);
|
|
406
|
+
if (isStartOfFrame) {
|
|
407
|
+
if (segmentLength < 7)
|
|
408
|
+
throw new Error("JPEG start-of-frame segment is too short.");
|
|
409
|
+
return validateDimensions(buffer.readUInt16BE(offset + 5), buffer.readUInt16BE(offset + 3));
|
|
410
|
+
}
|
|
411
|
+
offset += segmentLength;
|
|
412
|
+
}
|
|
413
|
+
throw new Error("JPEG dimensions were not found.");
|
|
414
|
+
}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { deflateSync, inflateSync } from "node:zlib";
|
|
2
|
+
const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
3
|
+
const MAX_PNG_PIXELS = 16_777_216;
|
|
4
|
+
const MAX_PNG_CHUNK_BYTES = 20 * 1024 * 1024;
|
|
5
|
+
export function decodePng(buffer) {
|
|
6
|
+
if (buffer.length < PNG_SIGNATURE.length || !buffer.subarray(0, 8).equals(PNG_SIGNATURE)) {
|
|
7
|
+
throw new Error("Pixel operations currently require a PNG image.");
|
|
8
|
+
}
|
|
9
|
+
let offset = PNG_SIGNATURE.length;
|
|
10
|
+
let header;
|
|
11
|
+
let palette;
|
|
12
|
+
let transparency;
|
|
13
|
+
const idatChunks = [];
|
|
14
|
+
let sawIend = false;
|
|
15
|
+
while (offset + 12 <= buffer.length) {
|
|
16
|
+
const length = buffer.readUInt32BE(offset);
|
|
17
|
+
if (length > MAX_PNG_CHUNK_BYTES) {
|
|
18
|
+
throw new Error(`PNG chunk exceeds ${MAX_PNG_CHUNK_BYTES} bytes.`);
|
|
19
|
+
}
|
|
20
|
+
const chunkEnd = offset + 12 + length;
|
|
21
|
+
if (chunkEnd > buffer.length)
|
|
22
|
+
throw new Error("PNG chunk extends beyond the file boundary.");
|
|
23
|
+
const type = buffer.toString("ascii", offset + 4, offset + 8);
|
|
24
|
+
const data = buffer.subarray(offset + 8, offset + 8 + length);
|
|
25
|
+
const expectedCrc = buffer.readUInt32BE(offset + 8 + length);
|
|
26
|
+
const actualCrc = crc32(Buffer.concat([Buffer.from(type, "ascii"), data]));
|
|
27
|
+
if (actualCrc !== expectedCrc)
|
|
28
|
+
throw new Error(`PNG ${type} chunk failed CRC validation.`);
|
|
29
|
+
if (type === "IHDR") {
|
|
30
|
+
if (header || length !== 13)
|
|
31
|
+
throw new Error("PNG must contain exactly one valid IHDR chunk.");
|
|
32
|
+
header = {
|
|
33
|
+
width: data.readUInt32BE(0),
|
|
34
|
+
height: data.readUInt32BE(4),
|
|
35
|
+
bitDepth: data[8],
|
|
36
|
+
colorType: data[9],
|
|
37
|
+
compression: data[10],
|
|
38
|
+
filter: data[11],
|
|
39
|
+
interlace: data[12],
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
else if (type === "PLTE") {
|
|
43
|
+
palette = Buffer.from(data);
|
|
44
|
+
}
|
|
45
|
+
else if (type === "tRNS") {
|
|
46
|
+
transparency = Buffer.from(data);
|
|
47
|
+
}
|
|
48
|
+
else if (type === "IDAT") {
|
|
49
|
+
idatChunks.push(Buffer.from(data));
|
|
50
|
+
}
|
|
51
|
+
else if (type === "IEND") {
|
|
52
|
+
sawIend = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
offset = chunkEnd;
|
|
56
|
+
}
|
|
57
|
+
if (!header || idatChunks.length === 0 || !sawIend) {
|
|
58
|
+
throw new Error("PNG is missing IHDR, IDAT, or IEND data.");
|
|
59
|
+
}
|
|
60
|
+
validateHeader(header);
|
|
61
|
+
const bytesPerPixel = bytesPerPixelForColorType(header.colorType);
|
|
62
|
+
const rowBytes = header.width * bytesPerPixel;
|
|
63
|
+
const expectedInflatedBytes = (rowBytes + 1) * header.height;
|
|
64
|
+
const inflated = inflateSync(Buffer.concat(idatChunks), {
|
|
65
|
+
maxOutputLength: expectedInflatedBytes,
|
|
66
|
+
});
|
|
67
|
+
if (inflated.length !== expectedInflatedBytes) {
|
|
68
|
+
throw new Error(`PNG decompressed to ${inflated.length} bytes; expected ${expectedInflatedBytes}.`);
|
|
69
|
+
}
|
|
70
|
+
const raw = unfilterRows(inflated, header.width, header.height, bytesPerPixel);
|
|
71
|
+
const rgba = convertToRgba(raw, header, palette, transparency);
|
|
72
|
+
return { width: header.width, height: header.height, data: rgba };
|
|
73
|
+
}
|
|
74
|
+
export function encodePng(image) {
|
|
75
|
+
validateDimensions(image.width, image.height);
|
|
76
|
+
const expectedBytes = image.width * image.height * 4;
|
|
77
|
+
if (image.data.length !== expectedBytes) {
|
|
78
|
+
throw new Error(`RGBA buffer contains ${image.data.length} bytes; expected ${expectedBytes}.`);
|
|
79
|
+
}
|
|
80
|
+
const rowBytes = image.width * 4;
|
|
81
|
+
const filtered = Buffer.alloc((rowBytes + 1) * image.height);
|
|
82
|
+
for (let y = 0; y < image.height; y++) {
|
|
83
|
+
const outputOffset = y * (rowBytes + 1);
|
|
84
|
+
filtered[outputOffset] = 0;
|
|
85
|
+
image.data.copy(filtered, outputOffset + 1, y * rowBytes, (y + 1) * rowBytes);
|
|
86
|
+
}
|
|
87
|
+
const ihdr = Buffer.alloc(13);
|
|
88
|
+
ihdr.writeUInt32BE(image.width, 0);
|
|
89
|
+
ihdr.writeUInt32BE(image.height, 4);
|
|
90
|
+
ihdr[8] = 8;
|
|
91
|
+
ihdr[9] = 6;
|
|
92
|
+
ihdr[10] = 0;
|
|
93
|
+
ihdr[11] = 0;
|
|
94
|
+
ihdr[12] = 0;
|
|
95
|
+
return Buffer.concat([
|
|
96
|
+
PNG_SIGNATURE,
|
|
97
|
+
createChunk("IHDR", ihdr),
|
|
98
|
+
createChunk("IDAT", deflateSync(filtered, { level: 9 })),
|
|
99
|
+
createChunk("IEND", Buffer.alloc(0)),
|
|
100
|
+
]);
|
|
101
|
+
}
|
|
102
|
+
function validateHeader(header) {
|
|
103
|
+
validateDimensions(header.width, header.height);
|
|
104
|
+
if (header.bitDepth !== 8)
|
|
105
|
+
throw new Error(`Unsupported PNG bit depth ${header.bitDepth}; only 8-bit PNGs are supported.`);
|
|
106
|
+
if (![0, 2, 3, 4, 6].includes(header.colorType)) {
|
|
107
|
+
throw new Error(`Unsupported PNG color type ${header.colorType}.`);
|
|
108
|
+
}
|
|
109
|
+
if (header.compression !== 0 || header.filter !== 0)
|
|
110
|
+
throw new Error("Unsupported PNG compression or filter method.");
|
|
111
|
+
if (header.interlace !== 0)
|
|
112
|
+
throw new Error("Interlaced PNGs are not supported for pixel operations.");
|
|
113
|
+
}
|
|
114
|
+
function validateDimensions(width, height) {
|
|
115
|
+
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 1 || height < 1) {
|
|
116
|
+
throw new Error(`Invalid PNG dimensions: ${width}x${height}.`);
|
|
117
|
+
}
|
|
118
|
+
if (width * height > MAX_PNG_PIXELS) {
|
|
119
|
+
throw new Error(`PNG dimensions exceed the ${MAX_PNG_PIXELS.toLocaleString("en-US")}-pixel safety limit.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function bytesPerPixelForColorType(colorType) {
|
|
123
|
+
switch (colorType) {
|
|
124
|
+
case 0: return 1;
|
|
125
|
+
case 2: return 3;
|
|
126
|
+
case 3: return 1;
|
|
127
|
+
case 4: return 2;
|
|
128
|
+
case 6: return 4;
|
|
129
|
+
default: throw new Error(`Unsupported PNG color type ${colorType}.`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
function unfilterRows(inflated, width, height, bytesPerPixel) {
|
|
133
|
+
const rowBytes = width * bytesPerPixel;
|
|
134
|
+
const output = Buffer.alloc(rowBytes * height);
|
|
135
|
+
for (let y = 0; y < height; y++) {
|
|
136
|
+
const inputOffset = y * (rowBytes + 1);
|
|
137
|
+
const filterType = inflated[inputOffset];
|
|
138
|
+
const row = inflated.subarray(inputOffset + 1, inputOffset + 1 + rowBytes);
|
|
139
|
+
const outputOffset = y * rowBytes;
|
|
140
|
+
for (let x = 0; x < rowBytes; x++) {
|
|
141
|
+
const encoded = row[x];
|
|
142
|
+
const left = x >= bytesPerPixel ? output[outputOffset + x - bytesPerPixel] : 0;
|
|
143
|
+
const up = y > 0 ? output[outputOffset + x - rowBytes] : 0;
|
|
144
|
+
const upperLeft = y > 0 && x >= bytesPerPixel
|
|
145
|
+
? output[outputOffset + x - rowBytes - bytesPerPixel]
|
|
146
|
+
: 0;
|
|
147
|
+
switch (filterType) {
|
|
148
|
+
case 0:
|
|
149
|
+
output[outputOffset + x] = encoded;
|
|
150
|
+
break;
|
|
151
|
+
case 1:
|
|
152
|
+
output[outputOffset + x] = (encoded + left) & 0xff;
|
|
153
|
+
break;
|
|
154
|
+
case 2:
|
|
155
|
+
output[outputOffset + x] = (encoded + up) & 0xff;
|
|
156
|
+
break;
|
|
157
|
+
case 3:
|
|
158
|
+
output[outputOffset + x] = (encoded + Math.floor((left + up) / 2)) & 0xff;
|
|
159
|
+
break;
|
|
160
|
+
case 4:
|
|
161
|
+
output[outputOffset + x] = (encoded + paeth(left, up, upperLeft)) & 0xff;
|
|
162
|
+
break;
|
|
163
|
+
default:
|
|
164
|
+
throw new Error(`Unsupported PNG row filter ${filterType}.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return output;
|
|
169
|
+
}
|
|
170
|
+
function convertToRgba(raw, header, palette, transparency) {
|
|
171
|
+
const pixels = header.width * header.height;
|
|
172
|
+
const rgba = Buffer.alloc(pixels * 4);
|
|
173
|
+
const sourceStride = bytesPerPixelForColorType(header.colorType);
|
|
174
|
+
if (header.colorType === 3 && (!palette || palette.length === 0 || palette.length % 3 !== 0)) {
|
|
175
|
+
throw new Error("Indexed PNG is missing a valid PLTE chunk.");
|
|
176
|
+
}
|
|
177
|
+
for (let pixel = 0; pixel < pixels; pixel++) {
|
|
178
|
+
const source = pixel * sourceStride;
|
|
179
|
+
const target = pixel * 4;
|
|
180
|
+
switch (header.colorType) {
|
|
181
|
+
case 0: {
|
|
182
|
+
const gray = raw[source];
|
|
183
|
+
rgba[target] = gray;
|
|
184
|
+
rgba[target + 1] = gray;
|
|
185
|
+
rgba[target + 2] = gray;
|
|
186
|
+
rgba[target + 3] = 255;
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
case 2:
|
|
190
|
+
rgba[target] = raw[source];
|
|
191
|
+
rgba[target + 1] = raw[source + 1];
|
|
192
|
+
rgba[target + 2] = raw[source + 2];
|
|
193
|
+
rgba[target + 3] = 255;
|
|
194
|
+
break;
|
|
195
|
+
case 3: {
|
|
196
|
+
const paletteIndex = raw[source];
|
|
197
|
+
const paletteOffset = paletteIndex * 3;
|
|
198
|
+
if (!palette || paletteOffset + 2 >= palette.length) {
|
|
199
|
+
throw new Error(`Indexed PNG references missing palette entry ${paletteIndex}.`);
|
|
200
|
+
}
|
|
201
|
+
rgba[target] = palette[paletteOffset];
|
|
202
|
+
rgba[target + 1] = palette[paletteOffset + 1];
|
|
203
|
+
rgba[target + 2] = palette[paletteOffset + 2];
|
|
204
|
+
rgba[target + 3] = transparency?.[paletteIndex] ?? 255;
|
|
205
|
+
break;
|
|
206
|
+
}
|
|
207
|
+
case 4: {
|
|
208
|
+
const gray = raw[source];
|
|
209
|
+
rgba[target] = gray;
|
|
210
|
+
rgba[target + 1] = gray;
|
|
211
|
+
rgba[target + 2] = gray;
|
|
212
|
+
rgba[target + 3] = raw[source + 1];
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
case 6:
|
|
216
|
+
raw.copy(rgba, target, source, source + 4);
|
|
217
|
+
break;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return rgba;
|
|
221
|
+
}
|
|
222
|
+
function paeth(left, up, upperLeft) {
|
|
223
|
+
const prediction = left + up - upperLeft;
|
|
224
|
+
const distanceLeft = Math.abs(prediction - left);
|
|
225
|
+
const distanceUp = Math.abs(prediction - up);
|
|
226
|
+
const distanceUpperLeft = Math.abs(prediction - upperLeft);
|
|
227
|
+
if (distanceLeft <= distanceUp && distanceLeft <= distanceUpperLeft)
|
|
228
|
+
return left;
|
|
229
|
+
if (distanceUp <= distanceUpperLeft)
|
|
230
|
+
return up;
|
|
231
|
+
return upperLeft;
|
|
232
|
+
}
|
|
233
|
+
function createChunk(type, data) {
|
|
234
|
+
const typeBuffer = Buffer.from(type, "ascii");
|
|
235
|
+
const chunk = Buffer.alloc(12 + data.length);
|
|
236
|
+
chunk.writeUInt32BE(data.length, 0);
|
|
237
|
+
typeBuffer.copy(chunk, 4);
|
|
238
|
+
data.copy(chunk, 8);
|
|
239
|
+
chunk.writeUInt32BE(crc32(Buffer.concat([typeBuffer, data])), 8 + data.length);
|
|
240
|
+
return chunk;
|
|
241
|
+
}
|
|
242
|
+
function crc32(buffer) {
|
|
243
|
+
let crc = 0xffffffff;
|
|
244
|
+
for (const byte of buffer) {
|
|
245
|
+
crc ^= byte;
|
|
246
|
+
for (let bit = 0; bit < 8; bit++) {
|
|
247
|
+
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
251
|
+
}
|