openpencil/tests/engine/io/pptx/export/helpers.ts
Danila Poyarkov 7f91594d0c refactor: standardize acronym casing
- Rename first-party API, RPC, JSON, CORS, SVG, JSX, and related identifiers to preserve acronym casing
- Keep upstream and serialized boundary names unchanged
- Add a lint guardrail and migration notes for exported APIs
2026-08-10 15:48:16 +03:00

83 lines
2.4 KiB
TypeScript

import { expect } from 'bun:test'
import { unzipSync } from 'fflate'
import { decodeBase64 } from '@open-pencil/core/bytes'
import type { Fill, Stroke } from '@open-pencil/scene-graph'
import { SceneGraph } from '@open-pencil/scene-graph'
// 1x1 transparent PNG — stub rasterizer output so unit tests avoid CanvasKit.
export const TINY_PNG = decodeBase64(
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=='
)
export const stubRasterize = () => Promise.resolve(TINY_PNG)
export function makeGraph(): SceneGraph {
return new SceneGraph()
}
export function solidFill(r: number, g: number, b: number, a = 1): Fill {
return { type: 'SOLID', color: { r, g, b, a }, opacity: 1, visible: true }
}
export function solidStroke(r: number, g: number, b: number, a = 1, weight = 2): Stroke {
return { color: { r, g, b, a }, weight, opacity: 1, visible: true, align: 'CENTER' }
}
export function pageId(graph: SceneGraph): string {
return graph.getPages()[0].id
}
export function makeSlideFrame(graph: SceneGraph, name: string) {
return graph.createNode('FRAME', pageId(graph), {
name,
width: 1280,
height: 720,
fills: [solidFill(1, 1, 1)]
})
}
export function unzipPPTX(data: Uint8Array): Record<string, Uint8Array> {
return unzipSync(data)
}
export function slideXML(files: Record<string, Uint8Array>, index: number): string {
const entry = files[`ppt/slides/slide${index}.xml`]
expect(entry).toBeDefined()
return new TextDecoder().decode(entry)
}
export const EMU_PER_INCH = 914400
export const SLIDE_WIDTH_IN = 13.333
/** Slide frames in these tests are 1280px wide, so px → in uses that scale. */
export function inches(px: number): number {
return px / (1280 / SLIDE_WIDTH_IN)
}
export interface Placement {
/** degrees, clockwise */
rot: number
centerX: number
centerY: number
width: number
height: number
}
/** Element placements (inches) read back from the slide XML. */
export function placements(xml: string): Placement[] {
const pattern =
/<a:xfrm(?: rot="(-?\d+)")?><a:off x="(-?\d+)" y="(-?\d+)"\/><a:ext cx="(-?\d+)" cy="(-?\d+)"\/><\/a:xfrm>/g
return [...xml.matchAll(pattern)].map((match) => {
const [x, y, width, height] = match.slice(2).map((value) => Number(value) / EMU_PER_INCH)
return {
rot: Number(match[1] ?? 0) / 60000,
centerX: x + width / 2,
centerY: y + height / 2,
width,
height
}
})
}