Add APNG Studio canvas extension#2272
Open
AndreaGriffiths11 wants to merge 6 commits into
Open
Conversation
APNG Studio is an interactive GitHub Copilot canvas extension for building Animated PNG (APNG) files from frames: draw or upload frames, tune per-frame timing and compositing, preview live, send the result to a phone by QR, and export an animated .png. Adds extensions/apng-studio/ with the required .github/plugin/plugin.json and assets/preview.png, and regenerates .github/plugin/marketplace.json. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
|
🔴 Contributor Reputation Check: HIGH risk
Maintainers: please review this contributor before merging. |
Contributor
There was a problem hiding this comment.
Pull request overview
Adds APNG Studio, a canvas extension for creating, previewing, sharing, and exporting animated PNGs.
Changes:
- Adds APNG and QR encoders with persistent project storage.
- Adds the interactive canvas UI and agent actions.
- Registers the extension in the marketplace.
Show a summary per file
| File | Description |
|---|---|
.github/plugin/marketplace.json |
Registers APNG Studio. |
extensions/apng-studio/.github/plugin/plugin.json |
Defines extension metadata. |
extensions/apng-studio/LICENSE |
Adds the MIT license. |
extensions/apng-studio/README.md |
Documents features and installation. |
extensions/apng-studio/apng.mjs |
Implements PNG/APNG encoding. |
extensions/apng-studio/assets/demo.png |
Provides the README demonstration. |
extensions/apng-studio/assets/preview.png |
Provides the marketplace preview. |
extensions/apng-studio/copilot-extension.json |
Defines runtime metadata. |
extensions/apng-studio/extension.mjs |
Implements storage, actions, servers, and sharing. |
extensions/apng-studio/qr.mjs |
Implements QR encoding. |
extensions/apng-studio/web/app.js |
Implements client-side behavior. |
extensions/apng-studio/web/index.html |
Defines the canvas interface. |
extensions/apng-studio/web/styles.css |
Styles the canvas interface. |
Review details
- Files reviewed: 11/13 changed files
- Comments generated: 9
- Review effort level: Medium
Security and robustness (extension.mjs):
- Reject "." and ".." project ids so canvas/action input cannot resolve
outside artifacts/.
- Bound request bodies (1 MiB JSON, 40 MiB frame upload); return 413 when
exceeded.
- Reject malformed JSON with 400 instead of coercing to {}, so a truncated
body cannot trigger destructive routes such as /frames/clear.
- Scope shares per project so one canvas cannot rotate or stop another's
share; the LAN server is shared and torn down once no shares remain.
- get_state treats hiddenFirst as active only with >=2 frames, matching the
encoder, so a single-frame project reports the correct duration.
Accessibility (web/):
- Expose Pen/Eraser state with aria-pressed and keep it in sync in setTool.
- Mark the toast as an aria-live status region.
Packaging:
- Add extensions/apng-studio/.gitignore with artifacts/ so the documented
runtime-data exclusion holds for this extension.
- Update the README install section to reference the committed awesome-copilot
extension path.
Regenerated .github/plugin/marketplace.json.
Update the plugin description (and regenerated marketplace entry) to refer to the host as the GitHub Copilot app.
Security: - Require a per-server access token on every loopback data/mutation request (minted per canvas server, carried in the iframe URL, attached to every renderer request). Static assets stay public. Blocks a local process or cross-origin page from reading state or driving mutations. Concurrency and durability: - Serialize the load-mutate-save cycle per project; dedupe concurrent first loads; write project.json via temp file + rename. Lifecycle: - Track SSE clients per instance and end them before server.close(). - Stop a project's LAN share only when no other panel references it. Correctness: - Report exact numerator/denominator total duration; handle pointercancel. QR and accessibility: - Place QR version bits least-significant-bit first (versions 7-10). - Associate delay/fps/dispose/blend labels with their inputs via for=.
- Run assemble() under the project lock; add_color_frame renders and appends within one lock. - Validate uploaded frames (PNG chunk scan, size + dimension checks) before writing; first frame stores real dimensions; CanvasError maps to 400. - Validate move delta; clear frames in memory before deleting files. - Route the open-handler rename through applySettings. - Deduplicate LAN share startup; clean up if the canvas closes mid-start. - End SSE streams before server.close(); skip dead streams in broadcast. - Exact fractional duration; drawing surface stays >= 1px; refresh state before re-seeding "start from last frame".
- Validate uploaded frames are 8-bit RGBA non-interlaced PNGs (standard compression/filter); reject formats the codec can't encode. - Cap projects at 600 frames (add + duplicate). - Bind the LAN share server to the private address, not 0.0.0.0; require a private address and rebind when it changes while idle; build the URL from the bound address. - Align width/height inputs and clampSize to the 2048 maximum. - Size the drawing surface by width + aspect ratio for narrow panels. - set_frame with an unknown frameId returns a validation error.
| const ARTIFACTS_DIR = join(EXT_DIR, "artifacts"); | ||
| const EXPORTS_DIR = join(ARTIFACTS_DIR, "exports"); | ||
| const DEFAULT_PROJECT = "default"; | ||
| const MAX_FRAMES = 600; // aggregate cap so a project can't exhaust the host on assemble |
Comment on lines
+1018
to
+1021
| delayMs: { type: "integer", minimum: 0, maximum: 65535 }, | ||
| fps: { type: "integer", minimum: 1, maximum: 1000, description: "Exact frame rate; sets delay to 1/fps s." }, | ||
| delayNum: { type: "integer", minimum: 0, maximum: 65535 }, | ||
| delayDen: { type: "integer", minimum: 1, maximum: 65535 }, |
Comment on lines
+699
to
+706
| for (const addrs of Object.values(networkInterfaces())) { | ||
| for (const a of addrs || []) { | ||
| if (a.family === "IPv4" && !a.internal) candidates.push(a.address); | ||
| } | ||
| } | ||
| const isPrivate = (ip) => | ||
| /^10\./.test(ip) || /^192\.168\./.test(ip) || /^172\.(1[6-9]|2\d|3[01])\./.test(ip); | ||
| return candidates.find(isPrivate) || null; |
Comment on lines
+59
to
+63
| const sanitizeId = (s) => { | ||
| const cleaned = String(s || DEFAULT_PROJECT).replace(/[^\w.-]+/g, "_").slice(0, 64); | ||
| // Reject "." and ".." (and empty) so a project id can never escape ARTIFACTS_DIR. | ||
| if (!cleaned || cleaned === "." || cleaned === "..") return DEFAULT_PROJECT; | ||
| return cleaned; |
Comment on lines
+102
to
+106
| try { | ||
| meta = JSON.parse(await fs.readFile(join(projectDir(pid), "project.json"), "utf8")); | ||
| } catch { | ||
| meta = null; | ||
| } |
Comment on lines
+239
to
+240
| delayNum: opts.delayNum != null ? opts.delayNum : opts.delayMs != null ? opts.delayMs : 120, | ||
| delayDen: opts.delayDen, |
| const bytes = await assembleFromMeta(meta); | ||
| if (!bytes) throw new CanvasError("no_frames", "Nothing to export — add at least one frame first."); | ||
| await ensureDir(EXPORTS_DIR); | ||
| const stamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
APNG Studio is an interactive GitHub Copilot canvas extension for building Animated PNG (APNG) files from frames. It renders in a side panel and is also driveable through agent actions.
Features:
dispose_opandblend_opfrom the APNG spec..png(byte-compatible with PNG, so every viewer can open it).The APNG codec and a dependency-free QR encoder are vendored (
apng.mjs,qr.mjs); no third-party runtime dependencies.Contribution type
Canvas extension, added at
extensions/apng-studio/with.github/plugin/plugin.jsonandassets/preview.png.Validation
npm run plugin:validate: extension apng-studio is valid.npm start: regenerated.github/plugin/marketplace.json; README requires no changes.