( buffer: Buffer, ext: 'docx' | 'pptx' | 'pdf' )
| 382 | * Returns null if the buffer cannot be parsed or yields no useful data. |
| 383 | */ |
| 384 | export async function extractDocumentStyle( |
| 385 | buffer: Buffer, |
| 386 | ext: 'docx' | 'pptx' | 'pdf' |
| 387 | ): Promise<DocumentStyleSummary | null> { |
| 388 | if (ext === 'pdf') { |
| 389 | return extractPdfStyle(buffer) |
| 390 | } |
| 391 | |
| 392 | if (buffer.length < 4) return null |
| 393 | for (let i = 0; i < 4; i++) { |
| 394 | if (buffer[i] !== ZIP_MAGIC[i]) return null |
| 395 | } |
| 396 | |
| 397 | try { |
| 398 | const JSZip = (await import('jszip')).default |
| 399 | const zip = await JSZip.loadAsync(buffer) |
| 400 | |
| 401 | const themePath = ext === 'docx' ? 'word/theme/theme1.xml' : 'ppt/theme/theme1.xml' |
| 402 | const themeFile = zip.file(themePath) |
| 403 | |
| 404 | let theme: DocumentStyleSummary['theme'] |
| 405 | if (themeFile) { |
| 406 | theme = parseThemeXml(await themeFile.async('string')) |
| 407 | } else if (ext === 'pptx') { |
| 408 | // PPTX without a theme is malformed — nothing useful to return |
| 409 | return null |
| 410 | } |
| 411 | // DOCX without a theme is valid (e.g. LibreOffice-generated); continue with styles only |
| 412 | |
| 413 | const summary: DocumentStyleSummary = { format: ext, ...(theme && { theme }) } |
| 414 | |
| 415 | if (ext === 'docx') { |
| 416 | const stylesFile = zip.file('word/styles.xml') |
| 417 | if (stylesFile) { |
| 418 | const { styles, defaults } = parseDocxStyles(await stylesFile.async('string'), theme?.fonts) |
| 419 | if (styles.length > 0) summary.styles = styles |
| 420 | if (defaults) summary.defaults = defaults |
| 421 | } |
| 422 | // If there's neither a theme nor any styles, there's nothing useful to return |
| 423 | if (!theme && !summary.styles?.length) return null |
| 424 | } |
| 425 | |
| 426 | if (ext === 'pptx') { |
| 427 | const presFile = zip.file('ppt/presentation.xml') |
| 428 | if (presFile) { |
| 429 | const { slideCount, aspectRatio } = parsePptxPresentation(await presFile.async('string')) |
| 430 | if (slideCount > 0) summary.slideCount = slideCount |
| 431 | summary.aspectRatio = aspectRatio |
| 432 | } |
| 433 | const masterFile = |
| 434 | zip.file('ppt/slideMasters/slideMaster1.xml') ?? |
| 435 | zip.file('ppt/slidemaster/slidemaster1.xml') |
| 436 | if (masterFile) { |
| 437 | const bg = parseSlideMasterBackground(await masterFile.async('string')) |
| 438 | if (bg) summary.background = bg |
| 439 | } |
| 440 | } |
| 441 |
no test coverage detected