@shimabell06/cloud-arch-icon-browser 0.2.0 → 0.3.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/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @shimabell06/cloud-arch-icon-browser
2
2
 
3
+ ## 0.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 46753f3: Add an Experimental Windows PowerPoint Copy all workflow for Tray items through a capability-scoped localhost bridge, with bounded transient PNG handling and perceived-size normalization. Physical Windows/PowerPoint paste validation remains tracked by #57 and is not yet a formal compatibility claim.
8
+ - 9aeba15: Use a stable canonical localhost origin for packaged runs, reuse an already-running matching local instance, and let supported browsers reopen a previously validated local ZIP through a persisted File System Access handle without storing package bytes.
9
+ - 2e1045e: Add a session-only Tray with quantity/order controls, multi-select and drag collection, reusable locally persisted Saved Sets, recently-used history, and frequently-used shortcuts.
10
+
11
+ ### Patch Changes
12
+
13
+ - 2666a3e: Reduce initial and scroll-time icon preview latency with shared prefetch observation and bounded viewport-prioritized preview work.
14
+
15
+ ## 0.2.1
16
+
17
+ ### Patch Changes
18
+
19
+ - 593c7b5: Deduplicate identical icon filenames in the global All icons results and search suggestions while preserving category-scoped entries.
20
+
3
21
  ## 0.2.0
4
22
 
5
23
  ### Minor Changes
package/README.md CHANGED
@@ -1,129 +1,107 @@
1
1
  # Cloud Arch Icon Browser
2
2
 
3
- A local, browser-based viewer for Microsoft Azure Architecture Icons.
3
+ Find Microsoft Azure Architecture Icons quickly and copy them into PowerPoint, Excel, or other compatible apps.
4
4
 
5
- Cloud Arch Icon Browser is an independent open-source project. It is not affiliated with, endorsed by, or sponsored by Microsoft.
5
+ ![Cloud Arch Icon Browser demo](./docs/assets/demo.gif)
6
6
 
7
- Microsoft Azure Architecture Icons are **not included** in this repository or npm package. Users download the latest official icon package directly from Microsoft and use those icons in accordance with Microsoft's terms.
7
+ Cloud Arch Icon Browser is an independent open-source project. It is not affiliated with, endorsed by, or sponsored by Microsoft. Microsoft Azure Architecture Icons are not included in this repository or npm package.
8
8
 
9
- ## Usage
9
+ ## Quick start
10
10
 
11
- ```bash
12
- npx @shimabell06/cloud-arch-icon-browser
13
- ```
14
-
15
- The command runs the latest published npm release, starts a temporary HTTP server bound to `127.0.0.1` on an available port, and opens the local web UI in the default browser. The ZIP selected by the user is processed locally and is not uploaded or persisted by the application.
16
-
17
- ## Features
11
+ 1. Download the official Azure Architecture Icons ZIP from [Microsoft Learn](https://learn.microsoft.com/en-us/azure/architecture/icons/).
12
+ 2. Run:
18
13
 
19
- The current product workflow includes:
14
+ ```bash
15
+ npx @shimabell06/cloud-arch-icon-browser
16
+ ```
20
17
 
21
- - Load a user-selected official Azure Architecture Icons ZIP.
22
- - Preserve and browse the ZIP folder hierarchy as categories.
23
- - Search display names, original filenames, and category paths with deterministic exact → prefix → substring ranking ahead of fuzzy matches.
24
- - Keep category scope explicit through a removable filter chip while preserving the active query.
25
- - Use keyboard-operable search autocomplete with icon previews, recent searches, and favorite shortcuts.
26
- - Save Favorites, recently opened icons, recent searches, theme, Grid/Compact preference, and sidebar state as local UI metadata without persisting the ZIP or SVG contents.
27
- - Switch between Grid and Compact icon layouts.
28
- - Copy an icon as a transparent 512×512 PNG for compatible clipboard workflows such as Windows PowerPoint and Excel.
29
- - Open a centered details dialog with real package metadata, Favorite, Copy image, Copy SVG where supported, and original SVG download actions.
30
- - Preserve original SVG bytes and filenames for download.
31
- - Explicitly select System / Light / Dark theme behavior.
32
- - Replace the active package without reloading the application.
33
- - Use responsive desktop and mobile navigation with keyboard and accessibility support.
34
- - Run locally through `npx` with no automatic runtime network access.
18
+ 3. Choose the downloaded ZIP in the browser.
35
19
 
36
- `Copy image` depends on browser Clipboard API support and permission. If image clipboard writes are unavailable or denied, the app reports the failure and the original SVG download remains available.
20
+ The command serves the app from the stable local origin `http://127.0.0.1:41731/` and opens it in your default browser. A second invocation reuses the same running app; if another process occupies that port, the CLI reports an error rather than switching origins. The selected ZIP is processed locally and its bytes are never uploaded or copied into application storage.
37
21
 
38
- ## Official icon package
22
+ On browsers that support the File System Access API, a successfully validated selection can remember only the local file handle in IndexedDB. A later launch can use `Open previous ZIP` to request/read that file under normal browser permission rules, and `Forget previous ZIP reference` removes the handle. The app does not trigger a file-permission prompt automatically on page load. Unsupported browsers continue to use the normal ZIP picker.
39
23
 
40
- Download the latest package from Microsoft Learn:
24
+ ## Features
41
25
 
42
- https://learn.microsoft.com/en-us/azure/architecture/icons/
26
+ - Fast search across icon names, original filenames, and category paths.
27
+ - Category browsing with explicit search filter chips.
28
+ - Favorites, recently used icons, recent searches, Frequently used shortcuts, Tray, Saved Sets, and Grid / Compact views.
29
+ - Centered icon details with real package metadata.
30
+ - Copy icons as transparent 512×512 PNG images for compatible clipboard workflows such as Windows PowerPoint and Excel.
31
+ - Copy original SVG source text where supported or download the original SVG with its original filename.
32
+ - Experimental Windows PowerPoint `Copy all` from the Tray in the packaged `npx` runtime, preserving Tray order and quantities without a flattened-image fallback.
33
+ - System, Light, and Dark themes with responsive keyboard-accessible navigation.
34
+ - Local-only package processing with no automatic runtime network access; supported browsers can remember only a local file reference for the previous ZIP.
43
35
 
44
- The app may provide manual links to Microsoft documentation, but it does not automatically download the icon package or contact Microsoft at runtime.
36
+ `Copy image` depends on browser Clipboard API support and permission. If image clipboard writes are unavailable or denied, the app reports the failure and the original SVG remains available for download.
45
37
 
46
- The latest package explicitly verified for this project is recorded in [`COMPATIBILITY.md`](./COMPATIBILITY.md). Compatibility is structural rather than tied to a hard-coded Microsoft ZIP version.
38
+ ### Experimental PowerPoint Copy all
47
39
 
48
- ## Project status
40
+ On Windows, the packaged `npx` runtime exposes an explicitly Experimental `Copy all` action in the Tray. It prepares up to 36 transient 512×512 PNG representations, preserves Tray order and quantities, asks desktop PowerPoint to copy them as a multi-shape selection, and then lets you paste once in PowerPoint. Generated PNGs and temporary Office files stay local and are deleted after the operation. There is no cloud service and no flattened combined-image fallback.
49
41
 
50
- Public releases are published on npm as `@shimabell06/cloud-arch-icon-browser`; `v0.1.0` was the first public release. Version-specific release history is recorded in [`CHANGELOG.md`](./CHANGELOG.md) and GitHub Releases rather than duplicated as a hard-coded "current version" in this README.
42
+ This workflow is **not yet formally supported** because real-machine Windows 11 + current Chromium + Microsoft 365 PowerPoint validation is still pending. The UI shows an Experimental warning on first use. If the later validation fails, the feature will be disabled or removed rather than replaced by a flattened image.
51
43
 
52
- The production package contains the localhost CLI and prebuilt Web UI; Microsoft icon ZIP/SVG assets are never bundled into the package. The application includes a React-independent core for ZIP validation, metadata/categories, search, durable local UI identity, lazy SVG previews, and original-file downloads. The production localhost CLI/static server uses loopback-only binding, Host/path validation, restrictive browser security headers, and clean shutdown behavior.
44
+ The feature is enabled by default for evaluation. To disable it locally, run this in the app's browser developer console and reload:
53
45
 
54
- Release and maintenance automation includes Linux browser E2E/accessibility/visual regression, Linux build/package checks, Windows/macOS packaged CLI smoke tests, npm package leak validation, Changesets release PRs, npm Trusted Publishing through GitHub Actions OIDC with provenance, registry/tarball availability verification, and a weekly Microsoft source-change watcher.
46
+ ```js
47
+ localStorage.setItem(
48
+ "cloud-arch-icon-browser:feature:powerpoint-copy-all",
49
+ "off",
50
+ );
51
+ ```
55
52
 
56
- The current official `Azure_Public_Service_Icons_V24.zip` has passed the production-parser compatibility verification recorded in [`COMPATIBILITY.md`](./COMPATIBILITY.md).
53
+ Delete that key (or set it to `on`) to return to the built-in default. The existing single-icon `Copy image` workflow remains the stable cross-platform baseline.
57
54
 
58
- See the [core integration notes](./docs/icon-package-core.md) for the core API and validation boundaries and [`docs/RELEASE.md`](./docs/RELEASE.md) for the steady-state release runbook.
55
+ True vector clipboard copy and direct browser-to-PowerPoint drag are not part of this Experimental release. The `Copy SVG source` action copies SVG markup text; it is not a vector-image clipboard operation.
59
56
 
60
- ## Development
57
+ ## Official Azure icons
61
58
 
62
- Supported Node.js versions are defined by `package.json`:
59
+ Download the current package from [Microsoft Learn](https://learn.microsoft.com/en-us/azure/architecture/icons/) and use the icons in accordance with Microsoft's terms.
63
60
 
64
- ```text
65
- ^22.22.2 || ^24.15.0 || >=26.0.0
66
- ```
61
+ The latest package explicitly verified by this project is recorded in [`COMPATIBILITY.md`](./COMPATIBILITY.md). The runtime validates package structure rather than hard-coding a specific Microsoft ZIP version.
67
62
 
68
- Node 25.x is intentionally unsupported by the current dependency set. For reproducible local development and CI, use the exact version in [`.node-version`](./.node-version), currently Node 24.20.0.
63
+ ## Development
69
64
 
70
- With nvm:
65
+ Use the Node.js version pinned in [`.node-version`](./.node-version):
71
66
 
72
67
  ```bash
73
- nvm install 24.20.0
74
- nvm use 24.20.0
68
+ nvm install
69
+ nvm use
75
70
  npm ci
76
71
  npm run dev
77
72
  ```
78
73
 
79
- Open the URL printed by Vite. Development and preview servers bind to `127.0.0.1`. Geist Sans Variable and all UI dependencies are bundled locally; the only external link in the shell opens Microsoft Learn when clicked.
80
-
81
- Core validation commands:
74
+ Common validation commands:
82
75
 
83
76
  ```bash
84
77
  npm run check
85
78
  npm run typecheck
86
79
  npm test
87
80
  npm run build
88
- npm run test:cli-smoke
89
- npm run test:cli-package-smoke
90
- npm run verify:package
91
- npm run verify:release-ready
81
+ npm run test:e2e
92
82
  ```
93
83
 
94
- Browser E2E uses only the project-owned dummy ZIP fixture generated by `scripts/ui-review/create_fixture.py`:
95
-
96
- ```bash
97
- python3 scripts/ui-review/create_fixture.py /tmp/cloud-arch-icon-browser-e2e-fixture.zip
98
- npx playwright install chromium
99
- E2E_FIXTURE=/tmp/cloud-arch-icon-browser-e2e-fixture.zip npm run test:e2e
100
- ```
84
+ ### Regenerate the README demo
101
85
 
102
- `npm run test:e2e:update` updates the deliberately small Linux visual baseline set and should only be used when an intentional visual change has been reviewed.
103
-
104
- To verify a maintainer-downloaded current official ZIP without committing it:
86
+ Install Playwright Chromium and `ffmpeg`, then run the recorder with a maintainer-downloaded official Azure Architecture Icons ZIP:
105
87
 
106
88
  ```bash
107
- npm run verify:official -- /path/to/latest-official.zip
89
+ npx playwright install chromium
90
+ AZURE_ICON_ZIP=/path/to/Azure_Public_Service_Icons.zip npm run docs:demo
108
91
  ```
109
92
 
110
- `check:fix` applies Biome fixes; `test:watch` runs Vitest interactively. `build` typechecks the project, writes the Web UI to `dist/`, and compiles the production CLI to `cli/`. After a build, `node cli/index.js` exercises the same localhost server path that the package binary uses. `preview` is a Vite development tool for inspecting the Web build and is not the production CLI/server.
93
+ The recorder starts the local app, performs the search details Copy image flow with Playwright, and writes `docs/assets/demo.gif`. The source ZIP and SVG files are never copied into the repository.
111
94
 
112
- ## Project documents
95
+ ## Project docs
113
96
 
114
- See:
115
-
116
- - [`DESIGN.md`](./DESIGN.md) — current product and architecture design contract.
117
- - [`AGENTS.md`](./AGENTS.md) — repository instructions for coding agents.
118
- - [`COMPATIBILITY.md`](./COMPATIBILITY.md) — official package compatibility and verification status.
119
- - [`CONTRIBUTING.md`](./CONTRIBUTING.md) — contribution workflow.
120
- - [`docs/RELEASE.md`](./docs/RELEASE.md) — release, Trusted Publishing, compatibility, and watcher runbook.
97
+ - [`DESIGN.md`](./DESIGN.md) — product and architecture design contract.
98
+ - [`CONTRIBUTING.md`](./CONTRIBUTING.md) — contribution workflow and development details.
99
+ - [`COMPATIBILITY.md`](./COMPATIBILITY.md) — official package compatibility status.
100
+ - [`docs/RELEASE.md`](./docs/RELEASE.md) — release and maintenance runbook.
121
101
  - [`SECURITY.md`](./SECURITY.md) — vulnerability reporting policy.
122
102
 
123
103
  ## License
124
104
 
125
- The source code for this project is licensed under the MIT License. See [`LICENSE`](./LICENSE).
126
-
127
- Third-party software and assets retain their own licenses. See [`THIRD_PARTY_NOTICES.md`](./THIRD_PARTY_NOTICES.md).
105
+ The project source code is licensed under the MIT License. See [`LICENSE`](./LICENSE).
128
106
 
129
- Microsoft Azure Architecture Icons are Microsoft assets and are not covered by this project's MIT License.
107
+ Third-party software and assets retain their own licenses. See [`THIRD_PARTY_NOTICES.md`](./THIRD_PARTY_NOTICES.md). Microsoft Azure Architecture Icons are Microsoft assets and are not covered by this project's MIT License.
@@ -0,0 +1,406 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomBytes } from "node:crypto";
3
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
4
+ import { tmpdir } from "node:os";
5
+ import { join } from "node:path";
6
+ export const POWERPOINT_CAPABILITY_PATH = "/__bridge/powerpoint/capability";
7
+ export const POWERPOINT_COPY_ALL_PATH = "/__bridge/powerpoint/copy-all";
8
+ export const POWERPOINT_CAPABILITY_HEADER = "x-cloud-arch-capability";
9
+ export const MAX_POWERPOINT_OBJECTS = 36;
10
+ const MAX_UNIQUE_IMAGES = 36;
11
+ const MAX_IMAGE_BYTES = 2 * 1024 * 1024;
12
+ const MAX_REQUEST_BYTES = 12 * 1024 * 1024;
13
+ const POWERPOINT_TIMEOUT_MS = 20_000;
14
+ const PNG_SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
15
+ class PowerPointAutomationError extends Error {
16
+ kind;
17
+ constructor(message, kind) {
18
+ super(message);
19
+ this.kind = kind;
20
+ this.name = "PowerPointAutomationError";
21
+ }
22
+ }
23
+ export function createPowerPointBridge(options = {}) {
24
+ const platform = options.platform ?? process.platform;
25
+ const capabilityToken = options.capabilityToken ?? randomBytes(32).toString("base64url");
26
+ const runner = options.runner ?? runPowerPointCopy;
27
+ let busy = false;
28
+ return {
29
+ matches: (rawUrl) => {
30
+ const pathname = requestPathname(rawUrl);
31
+ return (pathname === POWERPOINT_CAPABILITY_PATH ||
32
+ pathname === POWERPOINT_COPY_ALL_PATH);
33
+ },
34
+ handle: async (request, response, port) => {
35
+ const pathname = requestPathname(request.url);
36
+ if (pathname === POWERPOINT_CAPABILITY_PATH) {
37
+ if (request.method !== "GET") {
38
+ response.setHeader("Allow", "GET");
39
+ sendJson(response, 405, { error: "Method Not Allowed" });
40
+ return;
41
+ }
42
+ const available = platform === "win32";
43
+ sendJson(response, 200, {
44
+ available,
45
+ experimental: true,
46
+ maxObjects: MAX_POWERPOINT_OBJECTS,
47
+ capability: available ? capabilityToken : null,
48
+ reason: available ? null : "windows-npx-only",
49
+ });
50
+ return;
51
+ }
52
+ if (pathname !== POWERPOINT_COPY_ALL_PATH) {
53
+ sendJson(response, 404, { error: "Not Found" });
54
+ return;
55
+ }
56
+ if (request.method !== "POST") {
57
+ response.setHeader("Allow", "POST");
58
+ sendJson(response, 405, { error: "Method Not Allowed" });
59
+ return;
60
+ }
61
+ if (platform !== "win32") {
62
+ sendJson(response, 501, {
63
+ error: "PowerPoint Copy all is available only in the Windows npx runtime.",
64
+ });
65
+ return;
66
+ }
67
+ const expectedOrigin = `http://127.0.0.1:${port}`;
68
+ if (request.headers.origin !== expectedOrigin) {
69
+ sendJson(response, 403, { error: "Forbidden" });
70
+ return;
71
+ }
72
+ if (request.headers[POWERPOINT_CAPABILITY_HEADER] !== capabilityToken) {
73
+ sendJson(response, 403, { error: "Forbidden" });
74
+ return;
75
+ }
76
+ if (!isJsonContentType(request.headers["content-type"])) {
77
+ sendJson(response, 415, { error: "Expected application/json." });
78
+ return;
79
+ }
80
+ if (busy) {
81
+ sendJson(response, 409, {
82
+ error: "A PowerPoint copy operation is already in progress.",
83
+ });
84
+ return;
85
+ }
86
+ let items;
87
+ try {
88
+ const raw = await readBoundedBody(request);
89
+ items = parseCopyPayload(JSON.parse(raw));
90
+ }
91
+ catch (error) {
92
+ if (error instanceof RequestTooLargeError) {
93
+ sendJson(response, 413, { error: error.message });
94
+ return;
95
+ }
96
+ sendJson(response, 400, {
97
+ error: error instanceof Error ? error.message : "Invalid Copy all payload.",
98
+ });
99
+ return;
100
+ }
101
+ busy = true;
102
+ try {
103
+ await runner(items);
104
+ sendJson(response, 200, {
105
+ ok: true,
106
+ objectCount: items.reduce((sum, item) => sum + item.quantity, 0),
107
+ });
108
+ }
109
+ catch (error) {
110
+ const message = error instanceof PowerPointAutomationError
111
+ ? error.message
112
+ : "PowerPoint automation failed. Try again with desktop PowerPoint available.";
113
+ sendJson(response, 503, { error: message });
114
+ }
115
+ finally {
116
+ busy = false;
117
+ }
118
+ },
119
+ };
120
+ }
121
+ export function parseCopyPayload(value) {
122
+ if (!isRecord(value) || !Array.isArray(value.items)) {
123
+ throw new Error("Copy all payload must contain an items array.");
124
+ }
125
+ if (value.items.length === 0 || value.items.length > MAX_UNIQUE_IMAGES) {
126
+ throw new Error(`Copy all supports 1-${MAX_UNIQUE_IMAGES} unique images.`);
127
+ }
128
+ let total = 0;
129
+ const parsed = [];
130
+ for (const item of value.items) {
131
+ if (!isRecord(item))
132
+ throw new Error("Each Copy all item must be an object.");
133
+ const keys = Object.keys(item).sort();
134
+ if (keys.length !== 2 || keys[0] !== "pngBase64" || keys[1] !== "quantity") {
135
+ throw new Error("Copy all items may contain only pngBase64 and quantity.");
136
+ }
137
+ if (typeof item.quantity !== "number" ||
138
+ !Number.isSafeInteger(item.quantity) ||
139
+ item.quantity < 1 ||
140
+ item.quantity > MAX_POWERPOINT_OBJECTS) {
141
+ throw new Error("Copy all item quantity is invalid.");
142
+ }
143
+ if (typeof item.pngBase64 !== "string") {
144
+ throw new Error("Copy all image must be base64 PNG data.");
145
+ }
146
+ const png = decodePng(item.pngBase64);
147
+ total += item.quantity;
148
+ if (total > MAX_POWERPOINT_OBJECTS) {
149
+ throw new Error(`Copy all supports at most ${MAX_POWERPOINT_OBJECTS} objects.`);
150
+ }
151
+ parsed.push({ png, quantity: item.quantity });
152
+ }
153
+ return parsed;
154
+ }
155
+ async function runPowerPointCopy(items) {
156
+ const directory = await mkdtemp(join(tmpdir(), "cloud-arch-icon-browser-powerpoint-"));
157
+ try {
158
+ const manifestItems = [];
159
+ for (let index = 0; index < items.length; index += 1) {
160
+ const item = items[index];
161
+ if (item === undefined)
162
+ continue;
163
+ const path = join(directory, `${String(index).padStart(2, "0")}.png`);
164
+ await writeFile(path, item.png, { flag: "wx" });
165
+ manifestItems.push({ path, quantity: item.quantity });
166
+ }
167
+ const manifestPath = join(directory, "manifest.json");
168
+ await writeFile(manifestPath, JSON.stringify({
169
+ total: items.reduce((sum, item) => sum + item.quantity, 0),
170
+ items: manifestItems,
171
+ }), { encoding: "utf8", flag: "wx" });
172
+ await invokePowerPointAutomation(manifestPath);
173
+ }
174
+ finally {
175
+ await rm(directory, { recursive: true, force: true });
176
+ }
177
+ }
178
+ function invokePowerPointAutomation(manifestPath) {
179
+ return new Promise((resolvePromise, rejectPromise) => {
180
+ const encodedCommand = Buffer.from(POWERPOINT_COPY_SCRIPT, "utf16le").toString("base64");
181
+ const child = spawn("powershell.exe", [
182
+ "-NoLogo",
183
+ "-NoProfile",
184
+ "-NonInteractive",
185
+ "-ExecutionPolicy",
186
+ "Bypass",
187
+ "-EncodedCommand",
188
+ encodedCommand,
189
+ ], {
190
+ windowsHide: true,
191
+ stdio: ["ignore", "ignore", "pipe"],
192
+ env: { ...process.env, CAB_POWERPOINT_MANIFEST: manifestPath },
193
+ });
194
+ let stderr = "";
195
+ let settled = false;
196
+ const finish = (error) => {
197
+ if (settled)
198
+ return;
199
+ settled = true;
200
+ clearTimeout(timeout);
201
+ if (error)
202
+ rejectPromise(error);
203
+ else
204
+ resolvePromise();
205
+ };
206
+ child.stderr.on("data", (chunk) => {
207
+ stderr = `${stderr}${chunk.toString("utf8")}`.slice(-8192);
208
+ });
209
+ child.once("error", (error) => {
210
+ if ("code" in error && error.code === "ENOENT") {
211
+ finish(new PowerPointAutomationError("Windows PowerShell is unavailable, so PowerPoint Copy all cannot run.", "not-installed"));
212
+ return;
213
+ }
214
+ finish(new PowerPointAutomationError("PowerPoint automation could not be started.", "automation-failed"));
215
+ });
216
+ child.once("close", (code) => {
217
+ if (code === 0) {
218
+ finish();
219
+ return;
220
+ }
221
+ if (stderr.includes("CAB_POWERPOINT_NOT_INSTALLED")) {
222
+ finish(new PowerPointAutomationError("Desktop Microsoft PowerPoint was not found. Install/open desktop PowerPoint and try again.", "not-installed"));
223
+ return;
224
+ }
225
+ finish(new PowerPointAutomationError("PowerPoint could not prepare the multi-object clipboard. Close modal dialogs in PowerPoint and try again.", "automation-failed"));
226
+ });
227
+ const timeout = setTimeout(() => {
228
+ child.kill();
229
+ finish(new PowerPointAutomationError("PowerPoint Copy all timed out. Close PowerPoint dialogs and try again.", "timeout"));
230
+ }, POWERPOINT_TIMEOUT_MS);
231
+ timeout.unref();
232
+ });
233
+ }
234
+ function decodePng(base64) {
235
+ if (base64.length === 0 ||
236
+ base64.length % 4 !== 0 ||
237
+ !/^[A-Za-z0-9+/]*={0,2}$/.test(base64)) {
238
+ throw new Error("Copy all image contains invalid base64 data.");
239
+ }
240
+ const png = Buffer.from(base64, "base64");
241
+ if (png.length === 0 || png.length > MAX_IMAGE_BYTES) {
242
+ throw new Error("Copy all PNG size is outside the allowed range.");
243
+ }
244
+ if (png.length < PNG_SIGNATURE.length || !png.subarray(0, 8).equals(PNG_SIGNATURE)) {
245
+ throw new Error("Copy all accepts PNG images only.");
246
+ }
247
+ return png;
248
+ }
249
+ async function readBoundedBody(request) {
250
+ const declared = Number(request.headers["content-length"] ?? 0);
251
+ if (Number.isFinite(declared) && declared > MAX_REQUEST_BYTES) {
252
+ throw new RequestTooLargeError();
253
+ }
254
+ const chunks = [];
255
+ let total = 0;
256
+ for await (const chunk of request) {
257
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
258
+ total += buffer.length;
259
+ if (total > MAX_REQUEST_BYTES)
260
+ throw new RequestTooLargeError();
261
+ chunks.push(buffer);
262
+ }
263
+ if (total === 0)
264
+ throw new Error("Copy all payload is empty.");
265
+ return Buffer.concat(chunks).toString("utf8");
266
+ }
267
+ class RequestTooLargeError extends Error {
268
+ constructor() {
269
+ super("Copy all payload is too large.");
270
+ this.name = "RequestTooLargeError";
271
+ }
272
+ }
273
+ function requestPathname(rawUrl) {
274
+ if (!rawUrl?.startsWith("/") || rawUrl.startsWith("//"))
275
+ return null;
276
+ const queryIndex = rawUrl.indexOf("?");
277
+ return queryIndex === -1 ? rawUrl : rawUrl.slice(0, queryIndex);
278
+ }
279
+ function isJsonContentType(value) {
280
+ return value?.toLowerCase().split(";", 1)[0]?.trim() === "application/json";
281
+ }
282
+ function sendJson(response, statusCode, value) {
283
+ const body = Buffer.from(JSON.stringify(value));
284
+ response.statusCode = statusCode;
285
+ response.setHeader("Content-Type", "application/json; charset=utf-8");
286
+ response.setHeader("Content-Length", String(body.length));
287
+ response.setHeader("Cache-Control", "no-store");
288
+ response.end(body);
289
+ }
290
+ function isRecord(value) {
291
+ return typeof value === "object" && value !== null && !Array.isArray(value);
292
+ }
293
+ const POWERPOINT_COPY_SCRIPT = String.raw `
294
+ $ErrorActionPreference = "Stop"
295
+ Set-StrictMode -Version Latest
296
+ $MsoFalse = 0
297
+ $MsoTrue = -1
298
+ $PpLayoutBlank = 12
299
+ $manifestPath = $env:CAB_POWERPOINT_MANIFEST
300
+ $application = $null
301
+ $presentation = $null
302
+ $slide = $null
303
+ $shapeRange = $null
304
+ $ownedApplication = $false
305
+
306
+ function Release-ComObject {
307
+ param($Object)
308
+ if ($null -ne $Object -and [Runtime.InteropServices.Marshal]::IsComObject($Object)) {
309
+ try { [void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($Object) } catch {}
310
+ }
311
+ }
312
+
313
+ try {
314
+ if ([string]::IsNullOrWhiteSpace($manifestPath) -or -not (Test-Path -LiteralPath $manifestPath)) {
315
+ throw "Missing internal Copy all manifest."
316
+ }
317
+
318
+ try {
319
+ $application = [Runtime.InteropServices.Marshal]::GetActiveObject("PowerPoint.Application")
320
+ }
321
+ catch {
322
+ try {
323
+ $application = New-Object -ComObject PowerPoint.Application
324
+ $ownedApplication = $true
325
+ }
326
+ catch {
327
+ [Console]::Error.WriteLine("CAB_POWERPOINT_NOT_INSTALLED")
328
+ exit 21
329
+ }
330
+ }
331
+
332
+ $manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
333
+ $presentation = $application.Presentations.Add($MsoFalse)
334
+ $slide = $presentation.Slides.Add(1, $PpLayoutBlank)
335
+ $shapeNames = New-Object System.Collections.Generic.List[object]
336
+ $columns = [Math]::Ceiling([Math]::Sqrt([double]$manifest.total))
337
+ $index = 0
338
+
339
+ foreach ($item in $manifest.items) {
340
+ for ($copy = 0; $copy -lt [int]$item.quantity; $copy++) {
341
+ $column = $index % $columns
342
+ $row = [Math]::Floor($index / $columns)
343
+ $left = 10 + ($column * 86)
344
+ $top = 10 + ($row * 86)
345
+ $shape = $slide.Shapes.AddPicture(
346
+ [string]$item.path,
347
+ $MsoFalse,
348
+ $MsoTrue,
349
+ [single]$left,
350
+ [single]$top,
351
+ [single]72,
352
+ [single]72
353
+ )
354
+ try {
355
+ $null = $shapeNames.Add([string]$shape.Name)
356
+ }
357
+ finally {
358
+ Release-ComObject $shape
359
+ }
360
+ $index++
361
+ }
362
+ }
363
+
364
+ $shapeRange = $slide.Shapes.Range([object[]]$shapeNames.ToArray())
365
+ $shapeRange.Copy()
366
+ Start-Sleep -Milliseconds 350
367
+ $presentation.Saved = $MsoTrue
368
+ $presentation.Close()
369
+ Release-ComObject $shapeRange
370
+ $shapeRange = $null
371
+ Release-ComObject $slide
372
+ $slide = $null
373
+ Release-ComObject $presentation
374
+ $presentation = $null
375
+
376
+ if ($ownedApplication) {
377
+ $application.Quit()
378
+ }
379
+ Release-ComObject $application
380
+ $application = $null
381
+ [GC]::Collect()
382
+ [GC]::WaitForPendingFinalizers()
383
+ exit 0
384
+ }
385
+ catch {
386
+ [Console]::Error.WriteLine("CAB_POWERPOINT_AUTOMATION_FAILED")
387
+ exit 22
388
+ }
389
+ finally {
390
+ if ($null -ne $shapeRange) { Release-ComObject $shapeRange }
391
+ if ($null -ne $slide) { Release-ComObject $slide }
392
+ if ($null -ne $presentation) {
393
+ try {
394
+ $presentation.Saved = $MsoTrue
395
+ $presentation.Close()
396
+ } catch {}
397
+ Release-ComObject $presentation
398
+ }
399
+ if ($null -ne $application) {
400
+ if ($ownedApplication) { try { $application.Quit() } catch {} }
401
+ Release-ComObject $application
402
+ }
403
+ [GC]::Collect()
404
+ [GC]::WaitForPendingFinalizers()
405
+ }
406
+ `;