@ohos-ports/slides-grab 1.5.0-beta.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/LICENSE +21 -0
- package/README-ko.md +292 -0
- package/README.md +301 -0
- package/bin/ohos-postinstall.cjs +118 -0
- package/bin/ppt-agent.js +620 -0
- package/convert.cjs +20 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/glib-2.0/include/glibconfig.h +220 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/index.js +1 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libexpat.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libffi.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libjpeg.so +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libpcre2-8.so.0 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips-cpp.so.42.20.3 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/lib/libvips.so.42.20.3 +0 -0
- package/native/@img/sharp-libvips-openharmony-arm64/package.json +13 -0
- package/native/@img/sharp-openharmony-arm64/lib/sharp-openharmony-arm64.node +0 -0
- package/native/@img/sharp-openharmony-arm64/package.json +16 -0
- package/package.json +95 -0
- package/runtimes/claude-code/agents/design-critic-agent.md +23 -0
- package/runtimes/codex/agents/slides-grab-design-critic.md +22 -0
- package/scripts/build-viewer.js +444 -0
- package/scripts/design-gate.js +258 -0
- package/scripts/download-video.js +213 -0
- package/scripts/editor-server.js +1167 -0
- package/scripts/figma-export.js +169 -0
- package/scripts/generate-image.js +116 -0
- package/scripts/generate-images.js +164 -0
- package/scripts/html2pdf.js +822 -0
- package/scripts/html2png.js +246 -0
- package/scripts/html2pptx.js +162 -0
- package/scripts/import-template.js +86 -0
- package/scripts/install-runtime.js +216 -0
- package/scripts/render-tldraw.js +44 -0
- package/scripts/validate-slides.js +221 -0
- package/skills/slides-grab/SKILL.md +65 -0
- package/skills/slides-grab/references/presentation-workflow-reference.md +61 -0
- package/skills/slides-grab-card-news/SKILL.md +35 -0
- package/skills/slides-grab-design/SKILL.md +88 -0
- package/skills/slides-grab-design/references/beautiful-slide-defaults.md +88 -0
- package/skills/slides-grab-design/references/design-gate.md +349 -0
- package/skills/slides-grab-design/references/design-rules.md +76 -0
- package/skills/slides-grab-design/references/design-system-full.md +568 -0
- package/skills/slides-grab-design/references/detailed-design-rules.md +70 -0
- package/skills/slides-grab-export/SKILL.md +55 -0
- package/skills/slides-grab-export/references/export-rules.md +27 -0
- package/skills/slides-grab-export/references/html2pptx.md +627 -0
- package/skills/slides-grab-export/references/ooxml.md +427 -0
- package/skills/slides-grab-export/references/pptx-skill-reference.md +189 -0
- package/skills/slides-grab-html/SKILL.md +67 -0
- package/skills/slides-grab-image/SKILL.md +82 -0
- package/skills/slides-grab-plan/SKILL.md +68 -0
- package/skills/slides-grab-plan/references/design-md-to-slides-conversion.md +135 -0
- package/skills/slides-grab-plan/references/outline-format.md +47 -0
- package/skills/slides-grab-plan/references/plan-workflow-reference.md +140 -0
- package/src/codex-imagen.js +182 -0
- package/src/design-diversity-data.js +6932 -0
- package/src/design-gate-report.js +244 -0
- package/src/design-gate-state.js +329 -0
- package/src/design-import.js +164 -0
- package/src/design-md-parser.js +415 -0
- package/src/design-styles-data.js +1928 -0
- package/src/design-styles.js +209 -0
- package/src/editor/codex-edit.js +584 -0
- package/src/editor/edit-subprocess.js +170 -0
- package/src/editor/editor-codex-prompt.md +50 -0
- package/src/editor/editor.html +1785 -0
- package/src/editor/js/editor-bbox.js +332 -0
- package/src/editor/js/editor-chat.js +56 -0
- package/src/editor/js/editor-direct-edit.js +238 -0
- package/src/editor/js/editor-dom.js +59 -0
- package/src/editor/js/editor-init.js +405 -0
- package/src/editor/js/editor-navigation.js +54 -0
- package/src/editor/js/editor-select.js +558 -0
- package/src/editor/js/editor-send.js +175 -0
- package/src/editor/js/editor-sse.js +163 -0
- package/src/editor/js/editor-state.js +41 -0
- package/src/editor/js/editor-type.js +71 -0
- package/src/editor/js/editor-utils.js +167 -0
- package/src/editor/js/model-registry.js +37 -0
- package/src/editor/screenshot.js +82 -0
- package/src/export-resolution.cjs +68 -0
- package/src/figma.js +71 -0
- package/src/html2pptx-scale.cjs +120 -0
- package/src/html2pptx.cjs +1262 -0
- package/src/image-contract.js +329 -0
- package/src/image-native.js +468 -0
- package/src/nano-banana.js +841 -0
- package/src/ohos-browser-bridge.cjs +272 -0
- package/src/pptx-raster-export.cjs +299 -0
- package/src/resolve.js +110 -0
- package/src/slide-mode.cjs +72 -0
- package/src/template-fidelity.js +267 -0
- package/src/template-import.js +505 -0
- package/src/template-layout.js +261 -0
- package/src/template-pack.js +255 -0
- package/src/tldraw/render.js +473 -0
- package/src/validation/cli.js +120 -0
- package/src/validation/core.js +1023 -0
- package/templates/chart.html +121 -0
- package/templates/closing.html +54 -0
- package/templates/content.html +50 -0
- package/templates/contents.html +60 -0
- package/templates/cover.html +64 -0
- package/templates/custom/.gitkeep +0 -0
- package/templates/custom/README.md +7 -0
- package/templates/design-styles/README.md +20 -0
- package/templates/design-styles/preview.html +4438 -0
- package/templates/diagram-tldraw.html +56 -0
- package/templates/diagram.html +98 -0
- package/templates/quote.html +31 -0
- package/templates/section-divider.html +43 -0
- package/templates/split-layout.html +43 -0
- package/templates/statistics.html +55 -0
- package/templates/team.html +49 -0
- package/templates/timeline.html +59 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OHOS Browser Bridge — Launch system Chromium browser via hdc + connect via CDP
|
|
3
|
+
*
|
|
4
|
+
* Replaces playwright's chromium.launch() on HarmonyOS.
|
|
5
|
+
* Supports: Haitai browser (Chromium M132), Chrome 鸿蒙版, custom browsers.
|
|
6
|
+
*
|
|
7
|
+
* Based on browser-auto-migration (gitcode.com/zhenghongfei):
|
|
8
|
+
* 1. hdc shell aa force-stop (clean stale process)
|
|
9
|
+
* 2. hdc shell aa start --ps cmdArgs "--remote-debugging-port=PORT ..."
|
|
10
|
+
* 3. Wait for CDP endpoint (HTTP poll /json/version)
|
|
11
|
+
* 4. chromium.connectOverCDP(wsEndpoint)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const { execFileSync } = require('child_process');
|
|
15
|
+
const http = require('http');
|
|
16
|
+
|
|
17
|
+
// ─── Browser registry ────────────────────────────────────────────────────
|
|
18
|
+
// Supports multiple Chromium-based browsers on HarmonyOS
|
|
19
|
+
const CHANNEL_MAP = {
|
|
20
|
+
chrome: { pkg: 'com.haitai.htbrowser', ability: 'EntryAbility' },
|
|
21
|
+
'chrome-beta': { pkg: 'com.huawei.ohos_chromium', ability: 'BrowserAbility' },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const ENV_MAP = {
|
|
25
|
+
haitai: { pkg: 'com.haitai.htbrowser', ability: 'EntryAbility' },
|
|
26
|
+
chrome: { pkg: 'com.huawei.ohos_chromium', ability: 'BrowserAbility' },
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const DEFAULT_BROWSER = { pkg: 'com.haitai.htbrowser', ability: 'EntryAbility' };
|
|
30
|
+
|
|
31
|
+
function resolveBrowser(options = {}) {
|
|
32
|
+
// 1. Explicit bundle name
|
|
33
|
+
if (options.harmonyBundleName) {
|
|
34
|
+
return { pkg: options.harmonyBundleName, ability: options.harmonyAbilityName || 'EntryAbility' };
|
|
35
|
+
}
|
|
36
|
+
// 2. Channel mapping (chrome / chrome-beta)
|
|
37
|
+
if (options.channel && CHANNEL_MAP[options.channel]) {
|
|
38
|
+
return CHANNEL_MAP[options.channel];
|
|
39
|
+
}
|
|
40
|
+
// 3. Environment variable (HARMONY_BROWSER=haitai|chrome)
|
|
41
|
+
const env = process.env.HARMONY_BROWSER;
|
|
42
|
+
if (env && ENV_MAP[env]) {
|
|
43
|
+
return ENV_MAP[env];
|
|
44
|
+
}
|
|
45
|
+
// 4. Default
|
|
46
|
+
return DEFAULT_BROWSER;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ─── Chromium launch flags ──────────────────────────────────────────────
|
|
50
|
+
const BOOL_FLAGS = [
|
|
51
|
+
'disable-extensions', 'disable-default-apps', 'no-first-run',
|
|
52
|
+
'no-default-browser-check', 'disable-background-networking',
|
|
53
|
+
'disable-client-side-phishing-detection', 'disable-popup-blocking',
|
|
54
|
+
'disable-prompt-on-repost', 'disable-breakpad', 'disable-hang-monitor',
|
|
55
|
+
'disable-ipc-flooding-protection', 'metrics-recording-only', 'disable-sync',
|
|
56
|
+
'disable-search-engine-choice-screen', 'disable-background-timer-throttling',
|
|
57
|
+
'disable-renderer-backgrounding', 'disable-backgrounding-occluded-windows',
|
|
58
|
+
'disable-field-trial-config', 'disable-component-update', 'disable-dev-shm-usage',
|
|
59
|
+
'no-service-autorun', 'disable-infobars', 'allow-file-access-from-files',
|
|
60
|
+
'enable-automation', 'allow-pre-commit-input',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const DISABLED_FEATURES = [
|
|
64
|
+
'AvoidUnnecessaryBeforeUnloadCheckSync', 'DestroyProfileOnBrowserClose',
|
|
65
|
+
'DialMediaRouteProvider', 'GlobalMediaControls', 'HttpsUpgrades',
|
|
66
|
+
'LensOverlay', 'MediaRouter', 'PaintHolding',
|
|
67
|
+
'ThirdPartyStoragePartitioning', 'Translate', 'AutoDeElevate',
|
|
68
|
+
'RenderDocument', 'OptimizationHints',
|
|
69
|
+
].join(',');
|
|
70
|
+
|
|
71
|
+
// ─── hdc ────────────────────────────────────────────────────────────────
|
|
72
|
+
function getHdcPath() {
|
|
73
|
+
// Try standard HarmonyOS path first, then env var, then PATH
|
|
74
|
+
try { require('fs').accessSync('/data/service/hnp/bin/hdc'); return '/data/service/hnp/bin/hdc'; } catch {}
|
|
75
|
+
return process.env.HDC_PATH || 'hdc';
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function execHdc(args, timeout = 30000) {
|
|
79
|
+
return execFileSync(getHdcPath(), args, { timeout, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ensureDeviceConnected() {
|
|
83
|
+
const targets = execHdc(['list', 'targets']).split('\n').map(s => s.trim()).filter(Boolean);
|
|
84
|
+
if (targets.length > 0 && targets[0] !== '[Empty]') return;
|
|
85
|
+
try {
|
|
86
|
+
const port = parseInt(execFileSync('param', ['get', 'persist.hdc.port'], { timeout: 3000, encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(), 10);
|
|
87
|
+
if (port > 0 && execHdc(['tconn', `127.0.0.1:${port}`], 10000).includes('Connect OK')) return;
|
|
88
|
+
} catch {}
|
|
89
|
+
throw new Error('No HarmonyOS device connected. Enable Developer Options + Wireless Debugging.');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── CDP ────────────────────────────────────────────────────────────────
|
|
93
|
+
function buildCmdArgs(port, options = {}) {
|
|
94
|
+
const args = [
|
|
95
|
+
`--remote-debugging-port=${port}`,
|
|
96
|
+
`--remote-allow-origins=http://127.0.0.1:${port}`,
|
|
97
|
+
];
|
|
98
|
+
for (const f of BOOL_FLAGS) args.push(`--${f}`);
|
|
99
|
+
args.push('--password-store=basic', '--use-mock-keychain', '--force-color-profile=srgb');
|
|
100
|
+
args.push(`--disable-features=${DISABLED_FEATURES}`);
|
|
101
|
+
if (options.userDataDir) args.push(`--user-data-dir=${options.userDataDir}`);
|
|
102
|
+
return args.join(' ');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function tryCdpEndpoint(url) {
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
const req = http.get(url, (res) => {
|
|
108
|
+
let d = '';
|
|
109
|
+
res.on('data', (c) => d += c);
|
|
110
|
+
res.on('end', () => {
|
|
111
|
+
if (res.statusCode !== 200) return reject(new Error(`HTTP ${res.statusCode}`));
|
|
112
|
+
try {
|
|
113
|
+
const j = JSON.parse(d);
|
|
114
|
+
j.webSocketDebuggerUrl ? resolve(j.webSocketDebuggerUrl) : reject(new Error('No webSocketDebuggerUrl'));
|
|
115
|
+
} catch { reject(new Error('Parse failed')); }
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
req.on('error', reject);
|
|
119
|
+
req.setTimeout(2000, () => { req.destroy(); reject(new Error('Timeout')); });
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function waitForCdpEndpoint(port, timeoutMs = 30000) {
|
|
124
|
+
const start = Date.now();
|
|
125
|
+
while (Date.now() - start < timeoutMs) {
|
|
126
|
+
try {
|
|
127
|
+
return await tryCdpEndpoint(`http://127.0.0.1:${port}/json/version`);
|
|
128
|
+
} catch {
|
|
129
|
+
try {
|
|
130
|
+
return await tryCdpEndpoint(`http://[::1]:${port}/json/version`);
|
|
131
|
+
} catch {
|
|
132
|
+
await new Promise(r => setTimeout(r, 500));
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`CDP endpoint timeout on port ${port} after ${timeoutMs}ms`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ─── launchBrowserViaCdp ────────────────────────────────────────────────
|
|
140
|
+
// Cached browser instance (system browsers typically support single instance)
|
|
141
|
+
let _cachedBrowser = null;
|
|
142
|
+
let _launchPromise = null;
|
|
143
|
+
|
|
144
|
+
async function launchBrowserViaCdp(options = {}) {
|
|
145
|
+
if (_cachedBrowser) return _cachedBrowser;
|
|
146
|
+
if (_launchPromise) return _launchPromise;
|
|
147
|
+
|
|
148
|
+
_launchPromise = (async () => {
|
|
149
|
+
const { pkg, ability } = resolveBrowser(options);
|
|
150
|
+
const port = options.cdpPort || options.debuggingPort || 9333;
|
|
151
|
+
|
|
152
|
+
// 1. Connect to device
|
|
153
|
+
ensureDeviceConnected();
|
|
154
|
+
|
|
155
|
+
// 2. Check browser installed
|
|
156
|
+
if (!execHdc(['shell', 'bm', 'dump', '-a']).includes(pkg)) {
|
|
157
|
+
throw new Error(
|
|
158
|
+
`Browser "${pkg}" is not installed. ` +
|
|
159
|
+
`Install Haitai browser, or set HARMONY_BROWSER=chrome for Chrome 鸿蒙版, ` +
|
|
160
|
+
`or pass options.harmonyBundleName.`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 3. Stop stale browser process
|
|
165
|
+
try { execHdc(['shell', 'aa', 'force-stop', pkg]); } catch {}
|
|
166
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
167
|
+
|
|
168
|
+
// 4. Launch browser with CDP port
|
|
169
|
+
const cmdArgs = buildCmdArgs(port, options);
|
|
170
|
+
execHdc(['shell', 'aa', 'start', '-b', pkg, '-a', ability, '--ps', 'cmdArgs', cmdArgs]);
|
|
171
|
+
|
|
172
|
+
// 5. Wait for CDP endpoint
|
|
173
|
+
const wsEndpoint = await waitForCdpEndpoint(port);
|
|
174
|
+
|
|
175
|
+
// 6. Connect via Playwright connectOverCDP
|
|
176
|
+
const { chromium } = require('playwright');
|
|
177
|
+
const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
|
|
178
|
+
|
|
179
|
+
_cachedBrowser = browser;
|
|
180
|
+
|
|
181
|
+
// Override close to keep browser alive for reuse (single instance limitation)
|
|
182
|
+
browser.close = async () => {
|
|
183
|
+
// Don't actually close — keep cached for reuse. Process exit will clean up.
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Real cleanup on process exit
|
|
187
|
+
process.on('exit', () => {
|
|
188
|
+
try { execHdc(['shell', 'aa', 'force-stop', pkg]); } catch {}
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return browser;
|
|
192
|
+
})();
|
|
193
|
+
|
|
194
|
+
try {
|
|
195
|
+
return await _launchPromise;
|
|
196
|
+
} finally {
|
|
197
|
+
_launchPromise = null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── loadPageUrl ─────────────────────────────────────────────────────────
|
|
202
|
+
/**
|
|
203
|
+
* Load an HTML file into a page — uses setContent on HarmonyOS (file:// not accessible)
|
|
204
|
+
* Drop-in replacement for: await page.goto('file://' + filePath)
|
|
205
|
+
*/
|
|
206
|
+
async function loadPageUrl(page, url, options = {}) {
|
|
207
|
+
if (process.platform === 'openharmony' && url.startsWith('file://')) {
|
|
208
|
+
const filePath = url.replace(/^file:\/\//, '');
|
|
209
|
+
const fs = require('fs');
|
|
210
|
+
const html = fs.readFileSync(decodeURIComponent(filePath), 'utf-8');
|
|
211
|
+
await page.setContent(html, { waitUntil: options.waitUntil || 'load', timeout: options.timeout || 30000 });
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
await page.goto(url, options);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ─── Patch playwright-core (postinstall) ────────────────────────────────
|
|
218
|
+
/**
|
|
219
|
+
* Patch playwright-core to support openharmony platform.
|
|
220
|
+
* Called from bin/ohos-postinstall.cjs during npm install.
|
|
221
|
+
*/
|
|
222
|
+
function patchPlaywrightCore() {
|
|
223
|
+
const fs = require('fs');
|
|
224
|
+
const path = require('path');
|
|
225
|
+
|
|
226
|
+
let pcRoot;
|
|
227
|
+
try {
|
|
228
|
+
pcRoot = path.dirname(require.resolve('playwright-core/package.json'));
|
|
229
|
+
} catch {
|
|
230
|
+
console.log(' [ohos-bridge] playwright-core not found, skipping patch');
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const targetFile = path.join(pcRoot, 'lib', 'server', 'registry', 'index.js');
|
|
235
|
+
if (!fs.existsSync(targetFile)) {
|
|
236
|
+
console.log(' [ohos-bridge] registry/index.js not found, skipping patch');
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
let content = fs.readFileSync(targetFile, 'utf-8');
|
|
241
|
+
const MARKER = '/* @ohos-ports/slides-grab-patched */';
|
|
242
|
+
|
|
243
|
+
if (content.includes(MARKER)) {
|
|
244
|
+
console.log(' [ohos-bridge] Already patched');
|
|
245
|
+
return true;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Patch: add openharmony to platform check
|
|
249
|
+
const oldCode = `else if (process.platform === "win32")
|
|
250
|
+
cacheDirectory = process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local");
|
|
251
|
+
else
|
|
252
|
+
throw new Error("Unsupported platform: " + process.platform);`;
|
|
253
|
+
|
|
254
|
+
const newCode = `else if (process.platform === "win32")
|
|
255
|
+
cacheDirectory = process.env.LOCALAPPDATA || import_path.default.join(import_os.default.homedir(), "AppData", "Local");
|
|
256
|
+
else if (process.platform === "openharmony")
|
|
257
|
+
cacheDirectory = process.env.XDG_CACHE_HOME || import_path.default.join(import_os.default.homedir(), ".cache");
|
|
258
|
+
else
|
|
259
|
+
throw new Error("Unsupported platform: " + process.platform);`;
|
|
260
|
+
|
|
261
|
+
if (content.includes(oldCode)) {
|
|
262
|
+
content = MARKER + '\n' + content.replace(oldCode, newCode);
|
|
263
|
+
fs.writeFileSync(targetFile, content, 'utf-8');
|
|
264
|
+
console.log(' [ohos-bridge] ✓ Patched playwright-core registry/index.js');
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
console.log(' [ohos-bridge] ⚠ Could not find target code to patch (playwright version mismatch?)');
|
|
269
|
+
return false;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = { launchBrowserViaCdp, loadPageUrl, patchPlaywrightCore, resolveBrowser };
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
const PptxGenJS = require('pptxgenjs');
|
|
2
|
+
// OHOS: Use system Haitai browser via CDP bridge instead of standalone Chromium
|
|
3
|
+
const { launchBrowserViaCdp, loadPageUrl } = require('./ohos-browser-bridge.cjs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const sharp = require('sharp');
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
getResolutionChoices,
|
|
10
|
+
getResolutionSize,
|
|
11
|
+
normalizeResolutionPreset,
|
|
12
|
+
} = require('./export-resolution.cjs');
|
|
13
|
+
const {
|
|
14
|
+
DEFAULT_SLIDE_MODE,
|
|
15
|
+
getSlideModeChoices,
|
|
16
|
+
getSlideModeConfig,
|
|
17
|
+
normalizeSlideMode,
|
|
18
|
+
} = require('./slide-mode.cjs');
|
|
19
|
+
|
|
20
|
+
const DEFAULT_SLIDES_DIR = 'slides';
|
|
21
|
+
const DEFAULT_OUTPUT = 'output.pptx';
|
|
22
|
+
const DEFAULT_RESOLUTION = '2160p';
|
|
23
|
+
const DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR = 2;
|
|
24
|
+
const TARGET_RASTER_DPI = 150;
|
|
25
|
+
const SLIDE_FILE_PATTERN = /^slide-.*\.html$/i;
|
|
26
|
+
|
|
27
|
+
function normalizeDimension(value, fallback) {
|
|
28
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
29
|
+
return fallback;
|
|
30
|
+
}
|
|
31
|
+
return Math.max(1, Math.round(value));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildPageOptions(resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
|
|
35
|
+
const { framePx } = getSlideModeConfig(slideMode);
|
|
36
|
+
const targetResolution = getResolutionSize(resolution, slideMode);
|
|
37
|
+
return {
|
|
38
|
+
viewport: {
|
|
39
|
+
width: framePx.width,
|
|
40
|
+
height: framePx.height,
|
|
41
|
+
},
|
|
42
|
+
deviceScaleFactor: targetResolution
|
|
43
|
+
? targetResolution.height / framePx.height
|
|
44
|
+
: DEFAULT_CAPTURE_DEVICE_SCALE_FACTOR,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getTargetRasterSize(resolution = '', slideMode = DEFAULT_SLIDE_MODE) {
|
|
49
|
+
const targetResolution = getResolutionSize(resolution, slideMode);
|
|
50
|
+
if (targetResolution) {
|
|
51
|
+
return targetResolution;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const { pptxSizeIn } = getSlideModeConfig(slideMode);
|
|
55
|
+
return {
|
|
56
|
+
width: Math.round(pptxSizeIn.width * TARGET_RASTER_DPI),
|
|
57
|
+
height: Math.round(pptxSizeIn.height * TARGET_RASTER_DPI),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toSlideOrder(fileName) {
|
|
62
|
+
const match = fileName.match(/\d+/);
|
|
63
|
+
return match ? Number.parseInt(match[0], 10) : Number.POSITIVE_INFINITY;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function sortSlideFiles(a, b) {
|
|
67
|
+
const orderA = toSlideOrder(a);
|
|
68
|
+
const orderB = toSlideOrder(b);
|
|
69
|
+
if (orderA !== orderB) {
|
|
70
|
+
return orderA - orderB;
|
|
71
|
+
}
|
|
72
|
+
return a.localeCompare(b);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getHtmlSlides(slidesDir) {
|
|
76
|
+
if (!fs.existsSync(slidesDir)) {
|
|
77
|
+
throw new Error(`Slides directory not found: ${slidesDir}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const files = fs.readdirSync(slidesDir)
|
|
81
|
+
.filter((fileName) => SLIDE_FILE_PATTERN.test(fileName))
|
|
82
|
+
.sort(sortSlideFiles);
|
|
83
|
+
|
|
84
|
+
if (files.length === 0) {
|
|
85
|
+
throw new Error(`No slide-*.html files found in ${slidesDir}`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return files;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function printUsage() {
|
|
92
|
+
process.stdout.write(
|
|
93
|
+
[
|
|
94
|
+
'Usage: node convert.cjs [options]',
|
|
95
|
+
'',
|
|
96
|
+
'Options:',
|
|
97
|
+
` --slides-dir <path> Slide directory (default: ${DEFAULT_SLIDES_DIR})`,
|
|
98
|
+
` --output <path> Output pptx path (default: ${DEFAULT_OUTPUT})`,
|
|
99
|
+
` --mode <mode> Slide mode: ${getSlideModeChoices().join('|')} (default: ${DEFAULT_SLIDE_MODE})`,
|
|
100
|
+
` --resolution <preset> Raster size preset: ${getResolutionChoices().join('|')}|4k (default: ${DEFAULT_RESOLUTION})`,
|
|
101
|
+
' -h, --help Show this help message',
|
|
102
|
+
].join('\n'),
|
|
103
|
+
);
|
|
104
|
+
process.stdout.write('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function readOptionValue(args, index, optionName) {
|
|
108
|
+
const next = args[index + 1];
|
|
109
|
+
if (!next || next.startsWith('-')) {
|
|
110
|
+
throw new Error(`Missing value for ${optionName}.`);
|
|
111
|
+
}
|
|
112
|
+
return next;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseArgs(args) {
|
|
116
|
+
const options = {
|
|
117
|
+
slidesDir: DEFAULT_SLIDES_DIR,
|
|
118
|
+
output: DEFAULT_OUTPUT,
|
|
119
|
+
mode: DEFAULT_SLIDE_MODE,
|
|
120
|
+
resolution: DEFAULT_RESOLUTION,
|
|
121
|
+
help: false,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
125
|
+
const arg = args[i];
|
|
126
|
+
if (arg === '-h' || arg === '--help') {
|
|
127
|
+
options.help = true;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (arg === '--slides-dir') {
|
|
132
|
+
options.slidesDir = readOptionValue(args, i, '--slides-dir');
|
|
133
|
+
i += 1;
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (arg.startsWith('--slides-dir=')) {
|
|
138
|
+
options.slidesDir = arg.slice('--slides-dir='.length);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (arg === '--output') {
|
|
143
|
+
options.output = readOptionValue(args, i, '--output');
|
|
144
|
+
i += 1;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (arg.startsWith('--output=')) {
|
|
149
|
+
options.output = arg.slice('--output='.length);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (arg === '--mode') {
|
|
154
|
+
options.mode = normalizeSlideMode(readOptionValue(args, i, '--mode'));
|
|
155
|
+
i += 1;
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (arg.startsWith('--mode=')) {
|
|
160
|
+
options.mode = normalizeSlideMode(arg.slice('--mode='.length));
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (arg === '--resolution') {
|
|
165
|
+
options.resolution = normalizeResolutionPreset(readOptionValue(args, i, '--resolution'));
|
|
166
|
+
i += 1;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (arg.startsWith('--resolution=')) {
|
|
171
|
+
options.resolution = normalizeResolutionPreset(arg.slice('--resolution='.length));
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (typeof options.slidesDir !== 'string' || options.slidesDir.trim() === '') {
|
|
179
|
+
throw new Error('--slides-dir must be a non-empty string.');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (typeof options.output !== 'string' || options.output.trim() === '') {
|
|
183
|
+
throw new Error('--output must be a non-empty string.');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
options.slidesDir = options.slidesDir.trim();
|
|
187
|
+
options.output = options.output.trim();
|
|
188
|
+
options.mode = normalizeSlideMode(options.mode);
|
|
189
|
+
options.resolution = normalizeResolutionPreset(options.resolution);
|
|
190
|
+
return options;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function convertSlide(htmlFile, pres, browser, options = {}) {
|
|
194
|
+
const filePath = path.isAbsolute(htmlFile) ? htmlFile : path.join(process.cwd(), htmlFile);
|
|
195
|
+
const slideMode = normalizeSlideMode(options.mode || DEFAULT_SLIDE_MODE);
|
|
196
|
+
const fallbackSize = getSlideModeConfig(slideMode).framePx;
|
|
197
|
+
|
|
198
|
+
const page = await browser.newPage(buildPageOptions(options.resolution, slideMode));
|
|
199
|
+
await page.goto(`file://${filePath}`);
|
|
200
|
+
|
|
201
|
+
const bodyDimensions = await page.evaluate(() => {
|
|
202
|
+
const body = document.body;
|
|
203
|
+
const style = window.getComputedStyle(body);
|
|
204
|
+
return {
|
|
205
|
+
width: parseFloat(style.width),
|
|
206
|
+
height: parseFloat(style.height),
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await page.setViewportSize({
|
|
211
|
+
width: normalizeDimension(bodyDimensions.width, fallbackSize.width),
|
|
212
|
+
height: normalizeDimension(bodyDimensions.height, fallbackSize.height),
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
const screenshot = await page.screenshot({ type: 'png' });
|
|
216
|
+
await page.close();
|
|
217
|
+
|
|
218
|
+
const targetSize = getTargetRasterSize(options.resolution, slideMode);
|
|
219
|
+
|
|
220
|
+
const resized = await sharp(screenshot)
|
|
221
|
+
.resize(targetSize.width, targetSize.height, { fit: 'fill' })
|
|
222
|
+
.png()
|
|
223
|
+
.toBuffer();
|
|
224
|
+
|
|
225
|
+
const tmpPath = path.join(process.env.TMPDIR || '/tmp', `slide-${Date.now()}-${Math.random().toString(36).slice(2)}.png`);
|
|
226
|
+
fs.writeFileSync(tmpPath, resized);
|
|
227
|
+
|
|
228
|
+
const slide = pres.addSlide();
|
|
229
|
+
slide.addImage({
|
|
230
|
+
path: tmpPath,
|
|
231
|
+
x: 0,
|
|
232
|
+
y: 0,
|
|
233
|
+
w: '100%',
|
|
234
|
+
h: '100%',
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
return tmpPath;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function main(argv = process.argv.slice(2)) {
|
|
241
|
+
const options = parseArgs(argv);
|
|
242
|
+
if (options.help) {
|
|
243
|
+
printUsage();
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const pres = new PptxGenJS();
|
|
248
|
+
const { pptxSizeIn } = getSlideModeConfig(options.mode);
|
|
249
|
+
pres.defineLayout({
|
|
250
|
+
name: 'SLIDES_GRAB_DYNAMIC',
|
|
251
|
+
width: pptxSizeIn.width,
|
|
252
|
+
height: pptxSizeIn.height,
|
|
253
|
+
});
|
|
254
|
+
pres.layout = 'SLIDES_GRAB_DYNAMIC';
|
|
255
|
+
|
|
256
|
+
const slidesDir = path.resolve(process.cwd(), options.slidesDir);
|
|
257
|
+
const { ensureSlidesPassValidation } = await import('../scripts/validate-slides.js');
|
|
258
|
+
await ensureSlidesPassValidation(slidesDir, { exportLabel: 'PPTX export', slideMode: options.mode });
|
|
259
|
+
const files = getHtmlSlides(slidesDir);
|
|
260
|
+
|
|
261
|
+
console.log(`Converting ${files.length} slides...`);
|
|
262
|
+
|
|
263
|
+
const browser = process.platform === 'openharmony'
|
|
264
|
+
? await launchBrowserViaCdp()
|
|
265
|
+
: await require('playwright').chromium.launch();
|
|
266
|
+
const tmpFiles = [];
|
|
267
|
+
|
|
268
|
+
for (const file of files) {
|
|
269
|
+
const filePath = path.join(slidesDir, file);
|
|
270
|
+
console.log(` Processing: ${file}`);
|
|
271
|
+
try {
|
|
272
|
+
const tmpPath = await convertSlide(filePath, pres, browser, { mode: options.mode, resolution: options.resolution });
|
|
273
|
+
tmpFiles.push(tmpPath);
|
|
274
|
+
console.log(` ✓ ${file} done`);
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.error(` ✗ ${file} error: ${error.message}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
await browser.close();
|
|
281
|
+
|
|
282
|
+
const outputFile = path.resolve(process.cwd(), options.output);
|
|
283
|
+
await pres.writeFile({ fileName: outputFile });
|
|
284
|
+
console.log(`\nSaved: ${outputFile}`);
|
|
285
|
+
|
|
286
|
+
for (const filePath of tmpFiles) {
|
|
287
|
+
try {
|
|
288
|
+
fs.unlinkSync(filePath);
|
|
289
|
+
} catch {}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = {
|
|
294
|
+
buildPageOptions,
|
|
295
|
+
getHtmlSlides,
|
|
296
|
+
getTargetRasterSize,
|
|
297
|
+
main,
|
|
298
|
+
parseArgs,
|
|
299
|
+
};
|
package/src/resolve.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Path resolution for slides-grab.
|
|
3
|
+
*
|
|
4
|
+
* Resolution order:
|
|
5
|
+
* 1. Local (user's CWD) — per-project overrides
|
|
6
|
+
* 2. Package root — built-in defaults
|
|
7
|
+
*
|
|
8
|
+
* slides directory, slide-outline.md → always local (CWD)
|
|
9
|
+
* templates/ → local first, package fallback
|
|
10
|
+
* scripts/ → always package
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
14
|
+
import { dirname, join, resolve } from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const PACKAGE_ROOT = resolve(__dirname, '..');
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get the package root directory (where slides-grab is installed).
|
|
22
|
+
*/
|
|
23
|
+
export function getPackageRoot() {
|
|
24
|
+
return PACKAGE_ROOT;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the user's working directory (where slides are created).
|
|
29
|
+
*/
|
|
30
|
+
export function getCwd() {
|
|
31
|
+
return process.cwd();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the slides directory in CWD.
|
|
36
|
+
* @param {string} [slidesDir='slides'] - relative or absolute slides directory path
|
|
37
|
+
*/
|
|
38
|
+
export function getSlidesDir(slidesDir = process.env.PPT_AGENT_SLIDES_DIR || 'slides') {
|
|
39
|
+
return resolve(getCwd(), slidesDir);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Resolve a template file. Local first, then package fallback.
|
|
44
|
+
* @param {string} name — template name without extension (e.g. "cover")
|
|
45
|
+
* @returns {{ path: string, source: 'local' | 'package' } | null}
|
|
46
|
+
*/
|
|
47
|
+
export function resolveTemplate(name) {
|
|
48
|
+
const fileName = name.endsWith('.html') ? name : `${name}.html`;
|
|
49
|
+
|
|
50
|
+
const localPath = join(getCwd(), 'templates', fileName);
|
|
51
|
+
if (existsSync(localPath)) {
|
|
52
|
+
return { path: localPath, source: 'local' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const packagePath = join(PACKAGE_ROOT, 'templates', fileName);
|
|
56
|
+
if (existsSync(packagePath)) {
|
|
57
|
+
return { path: packagePath, source: 'package' };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* List all available templates (local + package, deduplicated).
|
|
65
|
+
* @returns {Array<{ name: string, source: 'local' | 'package' }>}
|
|
66
|
+
*/
|
|
67
|
+
export function listTemplates() {
|
|
68
|
+
const seen = new Map();
|
|
69
|
+
|
|
70
|
+
// Local templates first (take priority)
|
|
71
|
+
const localDir = join(getCwd(), 'templates');
|
|
72
|
+
if (existsSync(localDir)) {
|
|
73
|
+
for (const f of readdirSync(localDir)) {
|
|
74
|
+
if (f.endsWith('.html')) {
|
|
75
|
+
seen.set(f, { name: f.replace('.html', ''), source: 'local' });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Also check local custom/ subdirectory
|
|
81
|
+
const localCustomDir = join(localDir, 'custom');
|
|
82
|
+
if (existsSync(localCustomDir)) {
|
|
83
|
+
for (const f of readdirSync(localCustomDir)) {
|
|
84
|
+
if (f.endsWith('.html')) {
|
|
85
|
+
const key = `custom/${f}`;
|
|
86
|
+
seen.set(key, { name: `custom/${f.replace('.html', '')}`, source: 'local' });
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Package templates (only if not already overridden)
|
|
92
|
+
const pkgDir = join(PACKAGE_ROOT, 'templates');
|
|
93
|
+
if (existsSync(pkgDir)) {
|
|
94
|
+
for (const f of readdirSync(pkgDir)) {
|
|
95
|
+
if (f.endsWith('.html') && !seen.has(f)) {
|
|
96
|
+
seen.set(f, { name: f.replace('.html', ''), source: 'package' });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return Array.from(seen.values()).sort((a, b) => a.name.localeCompare(b.name));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a script path. Always from package.
|
|
106
|
+
* @param {string} relativePath — e.g. "scripts/validate-slides.js"
|
|
107
|
+
*/
|
|
108
|
+
export function resolveScript(relativePath) {
|
|
109
|
+
return join(PACKAGE_ROOT, relativePath);
|
|
110
|
+
}
|