jvs-ppt-ui 0.0.10 → 0.0.11
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/package.json +7 -7
- package/scripts/check_connectors.cjs +25 -0
- package/scripts/check_slide1.cjs +11 -0
- package/scripts/check_slide123.cjs +25 -0
- package/scripts/convert-fonts-to-woff2.cjs +97 -0
- package/scripts/extract-slides.ts +149 -0
- package/scripts/find_candidate_pptx.cjs +34 -0
- package/scripts/find_orange_slide.cjs +29 -0
- package/scripts/generate-complete-ppt-preview.js +385 -0
- package/scripts/inspect_315_327.cjs +19 -0
- package/scripts/mcp-smoke-test.js +568 -0
- package/scripts/patch-pptxtojson.cjs +81 -0
- package/scripts/verify-fonts.cjs +20 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jvs-ppt-ui",
|
|
3
3
|
"private": false,
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.11",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/lib/jvs-ppt-ui.umd.js",
|
|
7
7
|
"module": "./dist/lib/jvs-ppt-ui.es.js",
|
|
@@ -17,19 +17,19 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist/lib",
|
|
20
|
-
"src"
|
|
20
|
+
"src",
|
|
21
|
+
"scripts"
|
|
21
22
|
],
|
|
22
23
|
"scripts": {
|
|
23
24
|
"dev": "vite",
|
|
24
|
-
"build": "vue-tsc -b && vite build",
|
|
25
|
-
"build:web": "vite build --config vite.config.web.ts",
|
|
26
|
-
"build:lib": "vite build --config vite.config.lib.ts",
|
|
25
|
+
"build": "node scripts/patch-pptxtojson.cjs && vue-tsc -b && vite build",
|
|
26
|
+
"build:web": "node scripts/patch-pptxtojson.cjs && vite build --config vite.config.web.ts",
|
|
27
|
+
"build:lib": "node scripts/patch-pptxtojson.cjs && vite build --config vite.config.lib.ts",
|
|
27
28
|
"preview": "vite preview",
|
|
28
29
|
"parse": "node parse_pptx.cjs",
|
|
29
30
|
"mcp": "node mcp/server.js",
|
|
30
31
|
"mcp:http": "node mcp/server.js --http --host 0.0.0.0 --port 3209",
|
|
31
|
-
"mcp:smoke": "node scripts/mcp-smoke-test.js"
|
|
32
|
-
"postinstall": "node scripts/patch-pptxtojson.cjs"
|
|
32
|
+
"mcp:smoke": "node scripts/mcp-smoke-test.js"
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const JSZip = require('jszip');
|
|
3
|
+
|
|
4
|
+
async function test() {
|
|
5
|
+
const buf = fs.readFileSync('C:/Users/57087/Desktop/组合的元素7.pptx');
|
|
6
|
+
const zip = await JSZip.loadAsync(buf);
|
|
7
|
+
for (let i = 1; i <= 6; i++) {
|
|
8
|
+
const sXml = await zip.file('ppt/slides/slide' + i + '.xml')?.async('text');
|
|
9
|
+
if (!sXml) continue;
|
|
10
|
+
console.log(`=== Slide ${i} ===`);
|
|
11
|
+
const cxnMatches = sXml.match(/<p:cxnSp[\s\S]*?<\/p:cxnSp>/g) || [];
|
|
12
|
+
console.log(`Slide ${i} has ${cxnMatches.length} cxnSp`);
|
|
13
|
+
for (const m of cxnMatches) {
|
|
14
|
+
console.log(m);
|
|
15
|
+
}
|
|
16
|
+
const spMatches = sXml.match(/<p:sp[\s\S]*?<\/p:sp>/g) || [];
|
|
17
|
+
console.log(`Slide ${i} has ${spMatches.length} sp`);
|
|
18
|
+
for (const m of spMatches) {
|
|
19
|
+
const name = m.match(/name="([^"]+)"/)?.[1];
|
|
20
|
+
const prst = m.match(/prst="([^"]+)"/)?.[1];
|
|
21
|
+
console.log(` sp name=${name}, prst=${prst}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
test().catch(console.error);
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const JSZip = require('jszip');
|
|
3
|
+
|
|
4
|
+
async function test() {
|
|
5
|
+
const buf = fs.readFileSync('C:/Users/57087/Desktop/组合的元素7.pptx');
|
|
6
|
+
const zip = await JSZip.loadAsync(buf);
|
|
7
|
+
const sXml = await zip.file('ppt/slides/slide1.xml')?.async('text');
|
|
8
|
+
console.log('=== Slide 1 XML ===');
|
|
9
|
+
console.log(sXml);
|
|
10
|
+
}
|
|
11
|
+
test().catch(console.error);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const JSZip = require('jszip');
|
|
3
|
+
|
|
4
|
+
async function test() {
|
|
5
|
+
const buf = fs.readFileSync('C:/Users/57087/Desktop/组合的元素7.pptx');
|
|
6
|
+
const zip = await JSZip.loadAsync(buf);
|
|
7
|
+
for (const i of [1, 2, 3]) {
|
|
8
|
+
const sXml = await zip.file('ppt/slides/slide' + i + '.xml')?.async('text');
|
|
9
|
+
if (!sXml) continue;
|
|
10
|
+
console.log(`=== Slide ${i} ===`);
|
|
11
|
+
const cxnMatches = sXml.match(/<p:cxnSp[\s\S]*?<\/p:cxnSp>/g) || [];
|
|
12
|
+
console.log(`Slide ${i} cxnSp count: ${cxnMatches.length}`);
|
|
13
|
+
for (const m of cxnMatches) {
|
|
14
|
+
console.log(m);
|
|
15
|
+
}
|
|
16
|
+
const spMatches = sXml.match(/<p:sp[\s\S]*?<\/p:sp>/g) || [];
|
|
17
|
+
console.log(`Slide ${i} sp count: ${spMatches.length}`);
|
|
18
|
+
for (const m of spMatches) {
|
|
19
|
+
if (m.includes('Connector') || m.includes('connector') || m.includes('cxn')) {
|
|
20
|
+
console.log('Connector sp:', m);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
test().catch(console.error);
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 无损字体瘦身:public/jvs-ppt-ui/fonts 下所有 .ttf/.otf 转 .woff2(不做子集化,字形完整保留)
|
|
3
|
+
* - 重写 fonts.css:url 换 .woff2 + format('woff2'),去掉完全重复的 @font-face 块
|
|
4
|
+
* - 原始 ttf/otf 移到仓库根 fonts-src/(镜像目录结构),不再进 dist
|
|
5
|
+
* 用法:node scripts/convert-fonts-to-woff2.cjs
|
|
6
|
+
*/
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const path = require('path')
|
|
9
|
+
const wawoff2 = require('wawoff2')
|
|
10
|
+
|
|
11
|
+
const FONT_ROOT = path.join(__dirname, '..', 'public', 'jvs-ppt-ui', 'fonts')
|
|
12
|
+
const BACKUP_ROOT = path.join(__dirname, '..', 'fonts-src')
|
|
13
|
+
|
|
14
|
+
function fmtSize(n) {
|
|
15
|
+
return n > 1024 * 1024 ? (n / 1024 / 1024).toFixed(2) + 'MB' : (n / 1024).toFixed(0) + 'KB'
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function main() {
|
|
19
|
+
// 1. 收集全部 ttf/otf
|
|
20
|
+
const files = []
|
|
21
|
+
;(function walk(dir) {
|
|
22
|
+
for (const name of fs.readdirSync(dir)) {
|
|
23
|
+
const full = path.join(dir, name)
|
|
24
|
+
const st = fs.statSync(full)
|
|
25
|
+
if (st.isDirectory()) walk(full)
|
|
26
|
+
else if (/\.(ttf|otf)$/i.test(name)) files.push(full)
|
|
27
|
+
}
|
|
28
|
+
})(FONT_ROOT)
|
|
29
|
+
console.log(`发现 ${files.length} 个字体文件,开始转换...`)
|
|
30
|
+
|
|
31
|
+
// 2. 逐个压缩为同名 .woff2
|
|
32
|
+
let totalBefore = 0
|
|
33
|
+
let totalAfter = 0
|
|
34
|
+
const failed = []
|
|
35
|
+
for (const file of files) {
|
|
36
|
+
const buf = fs.readFileSync(file)
|
|
37
|
+
const out = file.replace(/\.(ttf|otf)$/i, '.woff2')
|
|
38
|
+
try {
|
|
39
|
+
const compressed = Buffer.from(await wawoff2.compress(buf))
|
|
40
|
+
fs.writeFileSync(out, compressed)
|
|
41
|
+
totalBefore += buf.length
|
|
42
|
+
totalAfter += compressed.length
|
|
43
|
+
console.log(` ok ${path.relative(FONT_ROOT, file)} ${fmtSize(buf.length)} -> ${fmtSize(compressed.length)}`)
|
|
44
|
+
} catch (e) {
|
|
45
|
+
failed.push({ file, error: e.message })
|
|
46
|
+
console.log(` FAIL ${path.relative(FONT_ROOT, file)}: ${e.message}`)
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
console.log(`\n转换完成(成功文件):${fmtSize(totalBefore)} -> ${fmtSize(totalAfter)}`)
|
|
50
|
+
if (failed.length) console.log(`失败 ${failed.length} 个(css 将保留其原引用)`)
|
|
51
|
+
|
|
52
|
+
// 3. 重写 fonts.css(失败文件保持原 url 不动)
|
|
53
|
+
const failedRel = new Set(failed.map(f => path.relative(FONT_ROOT, f.file).replace(/\\/g, '/')))
|
|
54
|
+
const cssPath = path.join(FONT_ROOT, 'fonts.css')
|
|
55
|
+
let css = fs.readFileSync(cssPath, 'utf8')
|
|
56
|
+
css = css.replace(/url\(([^)]*?\.(?:ttf|otf))\)\s*format\('(?:truetype|opentype)'\)/g, (m, url) => {
|
|
57
|
+
const decoded = decodeURIComponent(url.replace(/^['"]|['"]$/g, '')).replace(/^\/+/, '')
|
|
58
|
+
const rel = decoded.replace(/^jvs-ppt-ui\/fonts\//, '')
|
|
59
|
+
if (failedRel.has(rel)) return m
|
|
60
|
+
return `url(${url.replace(/\.(ttf|otf)$/i, '.woff2')}) format('woff2')`
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
// 4. 去掉完全相同的 @font-face 重复块
|
|
64
|
+
const blockRe = /@font-face\s*\{[^}]*\}/g
|
|
65
|
+
const seen = new Set()
|
|
66
|
+
let dedupCount = 0
|
|
67
|
+
css = css.replace(blockRe, (block) => {
|
|
68
|
+
const key = block.replace(/\s+/g, ' ')
|
|
69
|
+
if (seen.has(key)) {
|
|
70
|
+
dedupCount++
|
|
71
|
+
return ''
|
|
72
|
+
}
|
|
73
|
+
seen.add(key)
|
|
74
|
+
return block
|
|
75
|
+
})
|
|
76
|
+
css = css.replace(/\n{3,}/g, '\n\n')
|
|
77
|
+
fs.writeFileSync(cssPath, css)
|
|
78
|
+
console.log(`fonts.css 已更新(去除重复块 ${dedupCount} 个)`)
|
|
79
|
+
|
|
80
|
+
// 5. 移走原始 ttf/otf(仅当对应 woff2 已生成)
|
|
81
|
+
let moved = 0
|
|
82
|
+
for (const file of files) {
|
|
83
|
+
const woff2File = file.replace(/\.(ttf|otf)$/i, '.woff2')
|
|
84
|
+
if (!fs.existsSync(woff2File)) continue
|
|
85
|
+
const rel = path.relative(FONT_ROOT, file)
|
|
86
|
+
const dest = path.join(BACKUP_ROOT, rel)
|
|
87
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true })
|
|
88
|
+
fs.renameSync(file, dest)
|
|
89
|
+
moved++
|
|
90
|
+
}
|
|
91
|
+
console.log(`原始字体已移至 ${path.relative(process.cwd(), BACKUP_ROOT)}(${moved} 个)`)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
main().catch(e => {
|
|
95
|
+
console.error(e)
|
|
96
|
+
process.exit(1)
|
|
97
|
+
})
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import 'jvs-pptxtojson';
|
|
4
|
+
|
|
5
|
+
// 从全局挂载中读取解析函数
|
|
6
|
+
const parseFn = (globalThis as any).pptxtojson?.parse;
|
|
7
|
+
|
|
8
|
+
// 微信备份目录
|
|
9
|
+
const WECHAT_DIR = '/Users/guojing/Library/Containers/com.tencent.xinWeChat/Data/Documents/xwechat_files/aaguo521001_70b6/msg/file/2026-06';
|
|
10
|
+
// 前端模板输出目录
|
|
11
|
+
const OUTPUT_DIR = path.resolve('./public/templates');
|
|
12
|
+
const SLIDES_DIR = path.join(OUTPUT_DIR, 'slides');
|
|
13
|
+
|
|
14
|
+
// 极轻量级的透明 1x1 像素 GIF 占位符,防止巨大的 Base64 大图拖垮前端性能与打包体积
|
|
15
|
+
const TRANSPARENT_PLACEHOLDER = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
|
|
16
|
+
|
|
17
|
+
// 抽取单页中最核心的文字作为标题预览
|
|
18
|
+
function extractTitle(slide: any): string {
|
|
19
|
+
let bestText = '';
|
|
20
|
+
let maxFontSize = 0;
|
|
21
|
+
|
|
22
|
+
const checkText = (content: string) => {
|
|
23
|
+
if (!content) return;
|
|
24
|
+
// 匹配 HTML 标签内的文字
|
|
25
|
+
const cleanText = content.replace(/<[^>]+>/g, '').trim();
|
|
26
|
+
if (!cleanText || cleanText.length < 2) return;
|
|
27
|
+
|
|
28
|
+
// 尝试匹配 font-size: \d+pt
|
|
29
|
+
const sizeMatch = content.match(/font-size:\s*(\d+)pt/i);
|
|
30
|
+
const size = sizeMatch ? parseInt(sizeMatch[1]) : 12;
|
|
31
|
+
|
|
32
|
+
if (size > maxFontSize && cleanText.length < 40) {
|
|
33
|
+
maxFontSize = size;
|
|
34
|
+
bestText = cleanText;
|
|
35
|
+
} else if (!bestText && cleanText.length < 30) {
|
|
36
|
+
bestText = cleanText;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const elements = [...(slide.elements || []), ...(slide.layoutElements || [])];
|
|
41
|
+
for (const el of elements) {
|
|
42
|
+
if (el.content) {
|
|
43
|
+
checkText(el.content);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return bestText || '无文字页面';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 深度净化 Slide 结构,做防爆截断和大图占位替换
|
|
51
|
+
function sanitizeSlideData(slide: any) {
|
|
52
|
+
// 1. 清洗背景图
|
|
53
|
+
if (slide.fill && slide.fill.type === 'image' && slide.fill.value && slide.fill.value.base64) {
|
|
54
|
+
if (slide.fill.value.base64.length > 50000) { // 大于 50KB 替换
|
|
55
|
+
slide.fill.value.base64 = TRANSPARENT_PLACEHOLDER;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// 2. 清洗元素中的大图
|
|
60
|
+
const elements = [...(slide.elements || []), ...(slide.layoutElements || [])];
|
|
61
|
+
for (const el of elements) {
|
|
62
|
+
if (el.type === 'image' && el.src) {
|
|
63
|
+
if (el.src.startsWith('data:') && el.src.length > 20000) {
|
|
64
|
+
el.src = TRANSPARENT_PLACEHOLDER;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function run() {
|
|
71
|
+
if (!parseFn) {
|
|
72
|
+
console.error("Error: pptxtojson.parse is not loaded globally.");
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
console.log(`Scanning WeChat directory: ${WECHAT_DIR}`);
|
|
77
|
+
if (!fs.existsSync(WECHAT_DIR)) {
|
|
78
|
+
console.error(`wechat directory not found: ${WECHAT_DIR}`);
|
|
79
|
+
process.exit(1);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 递归查找 pptx 文件
|
|
83
|
+
const files = fs.readdirSync(WECHAT_DIR);
|
|
84
|
+
const pptxFiles = files.filter(f => f.toLowerCase().endsWith('.pptx') && !f.startsWith('~$'));
|
|
85
|
+
console.log(`Found ${pptxFiles.length} PPTX files to process.`);
|
|
86
|
+
|
|
87
|
+
// 创建输出目录
|
|
88
|
+
if (!fs.existsSync(OUTPUT_DIR)) fs.mkdirSync(OUTPUT_DIR, { recursive: true });
|
|
89
|
+
if (!fs.existsSync(SLIDES_DIR)) fs.mkdirSync(SLIDES_DIR, { recursive: true });
|
|
90
|
+
|
|
91
|
+
const indexList: any[] = [];
|
|
92
|
+
|
|
93
|
+
for (const file of pptxFiles) {
|
|
94
|
+
const filePath = path.join(WECHAT_DIR, file);
|
|
95
|
+
const pptName = path.basename(file, '.pptx');
|
|
96
|
+
console.log(`\n============================`);
|
|
97
|
+
console.log(`Parsing PPTX: ${file}`);
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
const buffer = fs.readFileSync(filePath);
|
|
101
|
+
const json = await parseFn(buffer, { imageMode: 'base64' });
|
|
102
|
+
const size = json.size || { width: 720, height: 405 };
|
|
103
|
+
const slides = json.slides || [];
|
|
104
|
+
console.log(`Successfully parsed: ${slides.length} slides, canvas size: ${size.width}x${size.height}`);
|
|
105
|
+
|
|
106
|
+
for (let i = 0; i < slides.length; i++) {
|
|
107
|
+
const rawSlide = slides[i];
|
|
108
|
+
const title = extractTitle(rawSlide);
|
|
109
|
+
|
|
110
|
+
// 净化大图 Base64 减小体积
|
|
111
|
+
sanitizeSlideData(rawSlide);
|
|
112
|
+
|
|
113
|
+
// 写入单个单页的原始 DSL
|
|
114
|
+
const slideFileName = `${pptName}_slide_${i + 1}.json`;
|
|
115
|
+
const slideFilePath = path.join(SLIDES_DIR, slideFileName);
|
|
116
|
+
|
|
117
|
+
fs.writeFileSync(slideFilePath, JSON.stringify({
|
|
118
|
+
originalSize: size,
|
|
119
|
+
slide: rawSlide
|
|
120
|
+
}, null, 2));
|
|
121
|
+
|
|
122
|
+
// 收集到索引元数据
|
|
123
|
+
indexList.push({
|
|
124
|
+
id: `${pptName}_${i + 1}`,
|
|
125
|
+
title: title,
|
|
126
|
+
pptSource: pptName,
|
|
127
|
+
slideIndex: i + 1,
|
|
128
|
+
fileName: slideFileName,
|
|
129
|
+
elementsCount: (rawSlide.elements || []).length + (rawSlide.layoutElements || []).length,
|
|
130
|
+
originalWidth: size.width,
|
|
131
|
+
originalHeight: size.height
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
console.log(`Saved ${slides.length} slides for ${file}`);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
console.error(`Failed to process PPTX ${file}:`, err);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// 写入前端统一索引用 index.json
|
|
141
|
+
const indexPath = path.join(OUTPUT_DIR, 'index.json');
|
|
142
|
+
fs.writeFileSync(indexPath, JSON.stringify({
|
|
143
|
+
totalSlides: indexList.length,
|
|
144
|
+
templates: indexList
|
|
145
|
+
}, null, 2));
|
|
146
|
+
console.log(`\nAll done! Successfully generated template index: ${indexPath} (Total slides: ${indexList.length})`);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
run().catch(console.error);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const JSZip = require('jszip');
|
|
4
|
+
|
|
5
|
+
async function checkFile(filePath) {
|
|
6
|
+
try {
|
|
7
|
+
const buf = fs.readFileSync(filePath);
|
|
8
|
+
const zip = await JSZip.loadAsync(buf);
|
|
9
|
+
const slideFiles = Object.keys(zip.files).filter(k => k.startsWith('ppt/slides/slide') && k.endsWith('.xml'));
|
|
10
|
+
for (const sf of slideFiles) {
|
|
11
|
+
const xml = await zip.file(sf)?.async('text');
|
|
12
|
+
if (!xml) continue;
|
|
13
|
+
// Check if it has 2 rects and 1 connector
|
|
14
|
+
if (xml.includes('bentConnector') || (xml.includes('cxnSp') && xml.includes('rect'))) {
|
|
15
|
+
const cxnCount = (xml.match(/<p:cxnSp/g) || []).length;
|
|
16
|
+
const spCount = (xml.match(/<p:sp/g) || []).length;
|
|
17
|
+
console.log(`Match in ${path.basename(filePath)} [${sf}]: spCount=${spCount}, cxnCount=${cxnCount}`);
|
|
18
|
+
if (spCount <= 5 && cxnCount <= 2) {
|
|
19
|
+
console.log(`*** CANDIDATE: ${path.basename(filePath)} [${sf}] ***`);
|
|
20
|
+
console.log(xml);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
} catch (e) {}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function main() {
|
|
28
|
+
const dir = 'C:/Users/57087/Desktop';
|
|
29
|
+
const files = fs.readdirSync(dir).filter(f => f.endsWith('.pptx'));
|
|
30
|
+
for (const f of files) {
|
|
31
|
+
await checkFile(path.join(dir, f));
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
main();
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const JSZip = require('jszip');
|
|
3
|
+
|
|
4
|
+
async function main() {
|
|
5
|
+
const dir = 'C:/Users/57087/Desktop';
|
|
6
|
+
const files = fs.readdirSync(dir);
|
|
7
|
+
for (const f of files) {
|
|
8
|
+
if (!f.endsWith('.pptx')) continue;
|
|
9
|
+
try {
|
|
10
|
+
const p = dir + '/' + f;
|
|
11
|
+
const zip = await JSZip.loadAsync(fs.readFileSync(p));
|
|
12
|
+
const slideFiles = Object.keys(zip.files).filter(k => k.startsWith('ppt/slides/slide') && k.endsWith('.xml'));
|
|
13
|
+
for (const sf of slideFiles) {
|
|
14
|
+
const xml = await zip.file(sf)?.async('text');
|
|
15
|
+
if (!xml) continue;
|
|
16
|
+
if (xml.includes('bentConnector2') || xml.includes('bentConnector3')) {
|
|
17
|
+
console.log(`===> ${f} [${sf}]`);
|
|
18
|
+
const matches = xml.match(/<p:cxnSp[\s\S]*?<\/p:cxnSp>/g) || [];
|
|
19
|
+
for (const m of matches) {
|
|
20
|
+
if (m.includes('bentConnector')) {
|
|
21
|
+
console.log(m);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
} catch(e) {}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
main();
|