| 12345678910111213141516171819202122232425262728293031 |
- import { readFile } from 'node:fs/promises'
- import JSZip from 'jszip'
- import { XMLParser } from 'fast-xml-parser'
- const parser = new XMLParser({ ignoreAttributes: false, removeNSPrefix: true })
- const list = <T>(value: T | T[] | undefined): T[] => value === undefined ? [] : Array.isArray(value) ? value : [value]
- function textNodes(value: unknown): string[] {
- if (!value || typeof value !== 'object') return []
- return Object.entries(value).flatMap(([key, child]) => key === 't'
- ? [typeof child === 'string' ? child : String(child ?? '')]
- : Array.isArray(child) ? child.flatMap(textNodes) : textNodes(child))
- }
- export async function extractPptxSpeakerNotes(path: string): Promise<string[]> {
- const zip = await JSZip.loadAsync(await readFile(path), { checkCRC32: true })
- const files = Object.keys(zip.files).filter(name => /^ppt\/notesSlides\/notesSlide\d+\.xml$/.test(name))
- .sort((a, b) => Number(a.match(/\d+(?=\.xml$)/)![0]) - Number(b.match(/\d+(?=\.xml$)/)![0]))
- if (files.length > 500) throw new Error('PPTX 页数超出备注解析限制')
- const notes: string[] = []
- for (const name of files) {
- const xml = await zip.file(name)!.async('string')
- const shapes = list(parser.parse(xml)?.notes?.cSld?.spTree?.sp)
- const body = shapes.find((shape: any) => shape?.nvSpPr?.nvPr?.ph?.['@_type'] === 'body') as any
- const paragraphs = list(body?.txBody?.p)
- .map((paragraph: unknown) => textNodes(paragraph).join('').trim())
- .filter(Boolean)
- notes.push(paragraphs.join('\n'))
- }
- return notes
- }
|