| 1 | import { afterEach, describe, expect, it } from 'vitest' |
| 2 | import { copyFile, mkdtemp, mkdir, rm, writeFile } from 'fs/promises' |
| 3 | import os from 'os' |
| 4 | import path from 'path' |
| 5 | import type { HtmlToPptxSlide } from '@arcsin1/html2pptx' |
| 6 | import { collectEmbeddedFonts } from '../../../src/main/io/html-pptx/font-collect' |
| 7 | |
| 8 | const tempDirectories: string[] = [] |
| 9 | |
| 10 | const readEotHeaderNames = (data: Uint8Array): { family: string; style: string } => { |
| 11 | const view = new DataView(data.buffer, data.byteOffset, data.byteLength) |
| 12 | let offset = 82 |
| 13 | const readName = (): string => { |
| 14 | const byteLength = view.getUint16(offset, true) |
| 15 | offset += 2 |
| 16 | const value = new TextDecoder('utf-16le').decode(data.slice(offset, offset + byteLength)) |
| 17 | offset += byteLength + 2 |
| 18 | return value |
| 19 | } |
| 20 | return { family: readName(), style: readName() } |
| 21 | } |
| 22 | |
| 23 | afterEach(async () => { |
| 24 | await Promise.all(tempDirectories.splice(0).map((directory) => rm(directory, { recursive: true, force: true }))) |
| 25 | }) |
| 26 | |
| 27 | describe('PPTX font embedding', () => { |
| 28 | it('embeds only the user font faces actually used by exported text', async () => { |
| 29 | const projectDir = await mkdtemp(path.join(os.tmpdir(), 'oh-my-ppt-font-')) |
| 30 | tempDirectories.push(projectDir) |
| 31 | const userFontsDir = path.join(projectDir, 'assets', 'fonts', 'user-fonts') |
| 32 | const titleDir = path.join(userFontsDir, 'title-font') |
| 33 | const bodyDir = path.join(userFontsDir, 'body-font') |
| 34 | await Promise.all([mkdir(titleDir, { recursive: true }), mkdir(bodyDir, { recursive: true })]) |
| 35 | |
| 36 | const regularSource = path.resolve('resources/google-fonts/Poppins/400-normal-0.woff2') |
| 37 | const boldSource = path.resolve('resources/google-fonts/Poppins/700-normal-0.woff2') |
| 38 | await Promise.all([ |
| 39 | copyFile(boldSource, path.join(titleDir, 'title.woff2')), |
| 40 | copyFile(regularSource, path.join(bodyDir, 'body.woff2')) |
| 41 | ]) |
| 42 | |
| 43 | const htmlPath = path.join(projectDir, 'page-1.html') |
| 44 | await writeFile( |
| 45 | htmlPath, |
| 46 | `<style> |
| 47 | @font-face { font-family: "User Title"; src: url("./assets/fonts/user-fonts/title-font/title.woff2") format("woff2"); font-weight: 700; font-style: normal; } |
| 48 | @font-face { font-family: "User Body"; src: url("./assets/fonts/user-fonts/body-font/body.woff2") format("woff2"); font-weight: 400; font-style: normal; } |
| 49 | </style>`, |
| 50 | 'utf-8' |
| 51 | ) |
| 52 | |
| 53 | const slides: HtmlToPptxSlide[] = [ |
| 54 | { |
| 55 | texts: [ |
| 56 | { text: 'Title', x: 0, y: 0, w: 2, h: 0.4, fontSize: 20, fontFace: 'User Title', bold: true }, |
| 57 | { text: 'Body', x: 0, y: 1, w: 2, h: 0.3, fontSize: 12, fontFace: 'User Body' } |
| 58 | ] |
| 59 | } |
| 60 | ] |
| 61 | |
| 62 | const embeddedFonts = await collectEmbeddedFonts(projectDir, slides, { |
| 63 | mode: 'always', |
| 64 | pageHtmlPaths: [htmlPath] |
| 65 | }) |
| 66 | |
| 67 | expect(embeddedFonts).toHaveLength(2) |
| 68 | expect(embeddedFonts.map((font) => [font.fontFace, font.style]).sort()).toEqual([ |
| 69 | ['User Body', 'regular'], |
| 70 | ['User Title', 'bold'] |
| 71 | ]) |
| 72 | expect(embeddedFonts.map((font) => readEotHeaderNames(font.ttfBuffer)).sort((a, b) => a.family.localeCompare(b.family))).toEqual([ |
| 73 | { family: 'User Body', style: 'Regular' }, |
| 74 | { family: 'User Title', style: 'Bold' } |
| 75 | ]) |
| 76 | }) |
| 77 | }) |
| 78 |