pptx-notes.ts 1.5 KB

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