chore: import upstream snapshot with attribution
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Parser for .rels (Relationship) XML files in OOXML packages.
|
||||
* These files map relationship IDs (rId1, rId2, ...) to targets.
|
||||
*/
|
||||
|
||||
import { parseXml } from './xml-parser'
|
||||
|
||||
export interface RelEntry {
|
||||
type: string
|
||||
target: string
|
||||
targetMode?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a .rels XML string into a Map of relationship ID -> RelEntry.
|
||||
*
|
||||
* Example input:
|
||||
* ```xml
|
||||
* <Relationships xmlns="...">
|
||||
* <Relationship Id="rId1" Type="http://...slide" Target="slides/slide1.xml"/>
|
||||
* </Relationships>
|
||||
* ```
|
||||
*/
|
||||
export function parseRels(xmlString: string): Map<string, RelEntry> {
|
||||
const result = new Map<string, RelEntry>()
|
||||
|
||||
if (!xmlString) return result
|
||||
|
||||
const root = parseXml(xmlString)
|
||||
if (!root.exists()) return result
|
||||
|
||||
const relationships = root.children('Relationship')
|
||||
for (const rel of relationships) {
|
||||
const id = rel.attr('Id')
|
||||
const type = rel.attr('Type')
|
||||
const target = rel.attr('Target')
|
||||
const targetMode = rel.attr('TargetMode')
|
||||
|
||||
if (id && type !== undefined && target !== undefined) {
|
||||
result.set(id, { type, target, targetMode })
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a relative target path against a base path.
|
||||
*
|
||||
* Examples:
|
||||
* resolveRelTarget('ppt/slides', '../slideLayouts/slideLayout1.xml')
|
||||
* → 'ppt/slideLayouts/slideLayout1.xml'
|
||||
*
|
||||
* resolveRelTarget('ppt/slides', 'media/image1.png')
|
||||
* → 'ppt/slides/media/image1.png'
|
||||
*
|
||||
* resolveRelTarget('ppt', 'slides/slide1.xml')
|
||||
* → 'ppt/slides/slide1.xml'
|
||||
*/
|
||||
export function resolveRelTarget(basePath: string, target: string): string {
|
||||
// Absolute targets (start with /) are returned as-is (strip leading /)
|
||||
if (target.startsWith('/')) {
|
||||
return target.slice(1)
|
||||
}
|
||||
|
||||
// Split the base path into segments
|
||||
const baseParts = basePath.replace(/\\/g, '/').split('/').filter(Boolean)
|
||||
const targetParts = target.replace(/\\/g, '/').split('/').filter(Boolean)
|
||||
|
||||
// Walk through target parts, resolving '..' by popping base parts
|
||||
const resolved = [...baseParts]
|
||||
for (const part of targetParts) {
|
||||
if (part === '..') {
|
||||
resolved.pop()
|
||||
} else if (part !== '.') {
|
||||
resolved.push(part)
|
||||
}
|
||||
}
|
||||
|
||||
return resolved.join('/')
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
/**
|
||||
* Unit conversion utilities for OOXML / PPTX.
|
||||
*
|
||||
* PPTX uses several unit systems:
|
||||
* - EMU (English Metric Units): 1 inch = 914400 EMU
|
||||
* - Points: 1 inch = 72 pt
|
||||
* - Hundredths of a point: used for font sizes
|
||||
* - 60000ths of a degree: used for angles
|
||||
* - 100000ths (percentage): used for scale factors
|
||||
*/
|
||||
|
||||
/** EMU to pixels (at 96 DPI). */
|
||||
export function emuToPx(emu: number): number {
|
||||
return (emu / 914400) * 96
|
||||
}
|
||||
|
||||
/** EMU to points. */
|
||||
export function emuToPt(emu: number): number {
|
||||
return emu / 12700
|
||||
}
|
||||
|
||||
/** OOXML angle (60000ths of a degree) to degrees. */
|
||||
export function angleToDeg(angle: number): number {
|
||||
return angle / 60000
|
||||
}
|
||||
|
||||
/** OOXML percentage (100000ths) to a decimal fraction (0..1 range for 0%..100%). */
|
||||
export function pctToDecimal(pct: number): number {
|
||||
return pct / 100000
|
||||
}
|
||||
|
||||
/** Hundredths of a point to points (used for font sizes in OOXML). */
|
||||
export function hundredthPtToPt(val: number): number {
|
||||
return val / 100
|
||||
}
|
||||
|
||||
/** Points to pixels (at 96 DPI). */
|
||||
export function ptToPx(pt: number): number {
|
||||
return (pt * 96) / 72
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristic: detect whether a value is in EMU or points.
|
||||
* Values with abs > 20000 are almost certainly EMU (a single point = 12700 EMU).
|
||||
*/
|
||||
export function detectUnit(value: number): 'emu' | 'point' {
|
||||
return Math.abs(value) > 20000 ? 'emu' : 'point'
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart conversion to pixels: auto-detects whether the value is EMU or points
|
||||
* and converts accordingly.
|
||||
*/
|
||||
export function smartToPx(value: number): number {
|
||||
if (detectUnit(value) === 'emu') {
|
||||
return emuToPx(value)
|
||||
}
|
||||
return ptToPx(value)
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
|
||||
const logger = createLogger('PptxXmlParser')
|
||||
|
||||
/**
|
||||
* Safe XML parser using browser DOMParser.
|
||||
* All operations are null-safe — accessing missing elements never crashes.
|
||||
*/
|
||||
|
||||
export class SafeXmlNode {
|
||||
private readonly el: Element | null
|
||||
|
||||
constructor(el: Element | null) {
|
||||
this.el = el
|
||||
}
|
||||
|
||||
/** Get a string attribute value, or undefined if missing. */
|
||||
attr(name: string): string | undefined {
|
||||
if (!this.el) return undefined
|
||||
return this.el.hasAttribute(name) ? this.el.getAttribute(name)! : undefined
|
||||
}
|
||||
|
||||
/** Get a numeric attribute value, or undefined if missing or not a number. */
|
||||
numAttr(name: string): number | undefined {
|
||||
const raw = this.attr(name)
|
||||
if (raw === undefined) return undefined
|
||||
const n = Number(raw)
|
||||
return Number.isNaN(n) ? undefined : n
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the first child element matching the given localName (namespace-agnostic).
|
||||
* Returns an empty SafeXmlNode if not found, so chaining never crashes.
|
||||
*/
|
||||
child(localName: string): SafeXmlNode {
|
||||
if (!this.el) return new SafeXmlNode(null)
|
||||
const children = this.el.children
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
if (children[i].localName === localName) {
|
||||
return new SafeXmlNode(children[i])
|
||||
}
|
||||
}
|
||||
return new SafeXmlNode(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get child elements, optionally filtered by localName (namespace-agnostic).
|
||||
* If no localName is given, returns all direct child elements.
|
||||
*/
|
||||
children(localName?: string): SafeXmlNode[] {
|
||||
if (!this.el) return []
|
||||
const result: SafeXmlNode[] = []
|
||||
const children = this.el.children
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
if (localName === undefined || children[i].localName === localName) {
|
||||
result.push(new SafeXmlNode(children[i]))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/** Get the text content, or empty string if the element is missing. */
|
||||
text(): string {
|
||||
if (!this.el) return ''
|
||||
return this.el.textContent ?? ''
|
||||
}
|
||||
|
||||
/** Whether the underlying element actually exists. */
|
||||
exists(): boolean {
|
||||
return this.el !== null
|
||||
}
|
||||
|
||||
/** All direct child elements as SafeXmlNode[]. */
|
||||
allChildren(): SafeXmlNode[] {
|
||||
return this.children()
|
||||
}
|
||||
|
||||
/** The localName of the underlying element, or empty string. */
|
||||
get localName(): string {
|
||||
return this.el?.localName ?? ''
|
||||
}
|
||||
|
||||
/** Raw access to the underlying Element (may be null). */
|
||||
get element(): Element | null {
|
||||
return this.el
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an XML string into a SafeXmlNode wrapping the document element.
|
||||
* Uses the browser's built-in DOMParser.
|
||||
*/
|
||||
export function parseXml(xmlString: string): SafeXmlNode {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(xmlString, 'application/xml')
|
||||
|
||||
// Check for parser errors — DOMParser returns a parsererror document on failure
|
||||
const errorNode = doc.querySelector('parsererror')
|
||||
if (errorNode) {
|
||||
logger.warn('XML parse error', { error: errorNode.textContent ?? '' })
|
||||
return new SafeXmlNode(null)
|
||||
}
|
||||
|
||||
return new SafeXmlNode(doc.documentElement)
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
import JSZip from 'jszip'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parseZip } from '@/lib/pptx-renderer/parser/zip-parser'
|
||||
|
||||
async function createZip(entries: Record<string, string | Uint8Array>): Promise<ArrayBuffer> {
|
||||
const zip = new JSZip()
|
||||
for (const [path, content] of Object.entries(entries)) {
|
||||
zip.file(path, content)
|
||||
}
|
||||
return zip.generateAsync({ type: 'arraybuffer' })
|
||||
}
|
||||
|
||||
describe('parseZip', () => {
|
||||
it('extracts PPTX package parts into categorized maps', async () => {
|
||||
const buffer = await createZip({
|
||||
'[Content_Types].xml': '<Types />',
|
||||
'ppt/presentation.xml': '<p:presentation />',
|
||||
'ppt/_rels/presentation.xml.rels': '<Relationships />',
|
||||
'ppt/slides/slide1.xml': '<p:sld />',
|
||||
'ppt/slides/_rels/slide1.xml.rels': '<Relationships />',
|
||||
'ppt/media/image1.png': new Uint8Array([1, 2, 3]),
|
||||
})
|
||||
|
||||
const files = await parseZip(buffer)
|
||||
|
||||
expect(files.contentTypes).toBe('<Types />')
|
||||
expect(files.presentation).toBe('<p:presentation />')
|
||||
expect(files.slides.get('ppt/slides/slide1.xml')).toBe('<p:sld />')
|
||||
expect(files.slideRels.get('ppt/slides/_rels/slide1.xml.rels')).toBe('<Relationships />')
|
||||
expect(files.media.get('ppt/media/image1.png')).toEqual(new Uint8Array([1, 2, 3]))
|
||||
})
|
||||
|
||||
it('rejects archives that exceed entry limits', async () => {
|
||||
const buffer = await createZip({
|
||||
'[Content_Types].xml': '<Types />',
|
||||
'ppt/presentation.xml': '<p:presentation />',
|
||||
})
|
||||
|
||||
await expect(parseZip(buffer, { maxEntries: 1 })).rejects.toThrow(
|
||||
'PPTX zip limit exceeded: entries 2 > maxEntries 1'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects archives that exceed media byte limits', async () => {
|
||||
const buffer = await createZip({
|
||||
'ppt/media/image1.png': new Uint8Array([1, 2, 3, 4]),
|
||||
})
|
||||
|
||||
await expect(parseZip(buffer, { maxMediaBytes: 3 })).rejects.toThrow('PPTX zip limit exceeded')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,269 @@
|
||||
/**
|
||||
* PPTX zip archive parser.
|
||||
* Extracts and categorizes all files from a .pptx (which is a zip archive).
|
||||
*/
|
||||
|
||||
import type { JSZipObject } from 'jszip'
|
||||
import JSZip from 'jszip'
|
||||
|
||||
export interface PptxFiles {
|
||||
contentTypes: string
|
||||
presentation: string
|
||||
presentationRels: string
|
||||
slides: Map<string, string>
|
||||
slideRels: Map<string, string>
|
||||
slideLayouts: Map<string, string>
|
||||
slideLayoutRels: Map<string, string>
|
||||
slideMasters: Map<string, string>
|
||||
slideMasterRels: Map<string, string>
|
||||
themes: Map<string, string>
|
||||
media: Map<string, Uint8Array>
|
||||
tableStyles?: string
|
||||
charts: Map<string, string> // ppt/charts/chart*.xml
|
||||
chartStyles: Map<string, string> // ppt/charts/style*.xml
|
||||
chartColors: Map<string, string> // ppt/charts/colors*.xml
|
||||
diagramDrawings: Map<string, string> // ppt/diagrams/drawing*.xml (SmartArt fallback)
|
||||
}
|
||||
|
||||
export interface ZipParseLimits {
|
||||
/** Maximum number of non-directory entries in the zip archive. */
|
||||
maxEntries?: number
|
||||
/** Maximum uncompressed size for any single entry (bytes). */
|
||||
maxEntryUncompressedBytes?: number
|
||||
/** Maximum total uncompressed size across all entries (bytes). */
|
||||
maxTotalUncompressedBytes?: number
|
||||
/** Maximum uncompressed size across media entries under `ppt/media/` (bytes). */
|
||||
maxMediaBytes?: number
|
||||
/** Maximum concurrent zip entry reads during parsing. */
|
||||
maxConcurrency?: number
|
||||
}
|
||||
|
||||
function throwZipLimitExceeded(reason: string): never {
|
||||
throw new Error(`PPTX zip limit exceeded: ${reason}`)
|
||||
}
|
||||
|
||||
function readUncompressedSize(file: JSZipObject): number | undefined {
|
||||
const data = (file as JSZipObject & { _data?: { uncompressedSize?: number } })._data
|
||||
const size = data?.uncompressedSize
|
||||
return typeof size === 'number' && Number.isFinite(size) ? size : undefined
|
||||
}
|
||||
|
||||
async function mapWithConcurrency<T>(
|
||||
items: T[],
|
||||
concurrency: number,
|
||||
mapper: (item: T) => Promise<void>
|
||||
): Promise<void> {
|
||||
if (items.length === 0) return
|
||||
const workerCount = Math.min(concurrency, items.length)
|
||||
let cursor = 0
|
||||
|
||||
const workers = Array.from({ length: workerCount }, async () => {
|
||||
while (true) {
|
||||
const index = cursor++
|
||||
if (index >= items.length) return
|
||||
await mapper(items[index])
|
||||
}
|
||||
})
|
||||
|
||||
await Promise.all(workers)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse a .pptx file buffer and extract all relevant files, categorized by type.
|
||||
*/
|
||||
export async function parseZip(
|
||||
buffer: ArrayBuffer,
|
||||
limits: ZipParseLimits = {}
|
||||
): Promise<PptxFiles> {
|
||||
const maxConcurrency = limits.maxConcurrency ?? 8
|
||||
if (!Number.isInteger(maxConcurrency) || maxConcurrency < 1) {
|
||||
throwZipLimitExceeded(`maxConcurrency ${limits.maxConcurrency} must be an integer >= 1`)
|
||||
}
|
||||
|
||||
const zip = await JSZip.loadAsync(buffer)
|
||||
const entries = Object.entries(zip.files).filter(([, file]) => !file.dir)
|
||||
|
||||
if (limits.maxEntries !== undefined && entries.length > limits.maxEntries) {
|
||||
throwZipLimitExceeded(`entries ${entries.length} > maxEntries ${limits.maxEntries}`)
|
||||
}
|
||||
|
||||
const knownSizeByPath = new Map<string, number>()
|
||||
let knownTotalBytes = 0
|
||||
let knownMediaBytes = 0
|
||||
|
||||
for (const [rawPath, file] of entries) {
|
||||
const normalizedPath = rawPath.replace(/\\/g, '/')
|
||||
const size = readUncompressedSize(file)
|
||||
if (size === undefined) continue
|
||||
|
||||
knownSizeByPath.set(normalizedPath, size)
|
||||
|
||||
if (limits.maxEntryUncompressedBytes !== undefined && size > limits.maxEntryUncompressedBytes) {
|
||||
throwZipLimitExceeded(
|
||||
`${normalizedPath} is ${size} bytes > maxEntryUncompressedBytes ${limits.maxEntryUncompressedBytes}`
|
||||
)
|
||||
}
|
||||
|
||||
knownTotalBytes += size
|
||||
if (
|
||||
limits.maxTotalUncompressedBytes !== undefined &&
|
||||
knownTotalBytes > limits.maxTotalUncompressedBytes
|
||||
) {
|
||||
throwZipLimitExceeded(
|
||||
`total uncompressed bytes ${knownTotalBytes} > maxTotalUncompressedBytes ${limits.maxTotalUncompressedBytes}`
|
||||
)
|
||||
}
|
||||
|
||||
if (normalizedPath.startsWith('ppt/media/')) {
|
||||
knownMediaBytes += size
|
||||
if (limits.maxMediaBytes !== undefined && knownMediaBytes > limits.maxMediaBytes) {
|
||||
throwZipLimitExceeded(
|
||||
`media bytes ${knownMediaBytes} > maxMediaBytes ${limits.maxMediaBytes}`
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const result: PptxFiles = {
|
||||
contentTypes: '',
|
||||
presentation: '',
|
||||
presentationRels: '',
|
||||
slides: new Map(),
|
||||
slideRels: new Map(),
|
||||
slideLayouts: new Map(),
|
||||
slideLayoutRels: new Map(),
|
||||
slideMasters: new Map(),
|
||||
slideMasterRels: new Map(),
|
||||
themes: new Map(),
|
||||
media: new Map(),
|
||||
charts: new Map(),
|
||||
chartStyles: new Map(),
|
||||
chartColors: new Map(),
|
||||
diagramDrawings: new Map(),
|
||||
}
|
||||
|
||||
let unknownMediaBytes = 0
|
||||
|
||||
await mapWithConcurrency(entries, maxConcurrency, async ([path, file]) => {
|
||||
const normalizedPath = path.replace(/\\/g, '/')
|
||||
|
||||
// --- Content Types ---
|
||||
if (normalizedPath === '[Content_Types].xml') {
|
||||
result.contentTypes = await file.async('string')
|
||||
return
|
||||
}
|
||||
|
||||
// --- Presentation ---
|
||||
if (normalizedPath === 'ppt/presentation.xml') {
|
||||
result.presentation = await file.async('string')
|
||||
return
|
||||
}
|
||||
|
||||
// --- Presentation Rels ---
|
||||
if (normalizedPath === 'ppt/_rels/presentation.xml.rels') {
|
||||
result.presentationRels = await file.async('string')
|
||||
return
|
||||
}
|
||||
|
||||
// --- Table Styles ---
|
||||
if (normalizedPath === 'ppt/tableStyles.xml') {
|
||||
result.tableStyles = await file.async('string')
|
||||
return
|
||||
}
|
||||
|
||||
// --- Media (binary) ---
|
||||
if (normalizedPath.startsWith('ppt/media/')) {
|
||||
const bytes = await file.async('uint8array')
|
||||
if (!knownSizeByPath.has(normalizedPath)) {
|
||||
const size = bytes.byteLength
|
||||
if (
|
||||
limits.maxEntryUncompressedBytes !== undefined &&
|
||||
size > limits.maxEntryUncompressedBytes
|
||||
) {
|
||||
throwZipLimitExceeded(
|
||||
`${normalizedPath} is ${size} bytes > maxEntryUncompressedBytes ${limits.maxEntryUncompressedBytes}`
|
||||
)
|
||||
}
|
||||
unknownMediaBytes += size
|
||||
if (
|
||||
limits.maxMediaBytes !== undefined &&
|
||||
knownMediaBytes + unknownMediaBytes > limits.maxMediaBytes
|
||||
) {
|
||||
throwZipLimitExceeded(
|
||||
`media bytes ${knownMediaBytes + unknownMediaBytes} > maxMediaBytes ${limits.maxMediaBytes}`
|
||||
)
|
||||
}
|
||||
}
|
||||
result.media.set(normalizedPath, bytes)
|
||||
return
|
||||
}
|
||||
|
||||
// --- Slide Rels (must check before slides to avoid false match) ---
|
||||
if (/^ppt\/slides\/_rels\/slide\d+\.xml\.rels$/.test(normalizedPath)) {
|
||||
result.slideRels.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Slides ---
|
||||
if (/^ppt\/slides\/slide\d+\.xml$/.test(normalizedPath)) {
|
||||
result.slides.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Slide Layout Rels ---
|
||||
if (/^ppt\/slideLayouts\/_rels\/slideLayout\d+\.xml\.rels$/.test(normalizedPath)) {
|
||||
result.slideLayoutRels.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Slide Layouts ---
|
||||
if (/^ppt\/slideLayouts\/slideLayout\d+\.xml$/.test(normalizedPath)) {
|
||||
result.slideLayouts.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Slide Master Rels ---
|
||||
if (/^ppt\/slideMasters\/_rels\/slideMaster\d+\.xml\.rels$/.test(normalizedPath)) {
|
||||
result.slideMasterRels.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Slide Masters ---
|
||||
if (/^ppt\/slideMasters\/slideMaster\d+\.xml$/.test(normalizedPath)) {
|
||||
result.slideMasters.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Themes ---
|
||||
if (/^ppt\/theme\/theme\d+\.xml$/.test(normalizedPath)) {
|
||||
result.themes.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Charts ---
|
||||
if (/^ppt\/charts\/chart\d+\.xml$/.test(normalizedPath)) {
|
||||
result.charts.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Chart Styles ---
|
||||
if (/^ppt\/charts\/style\d+\.xml$/.test(normalizedPath)) {
|
||||
result.chartStyles.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Chart Colors ---
|
||||
if (/^ppt\/charts\/colors\d+\.xml$/.test(normalizedPath)) {
|
||||
result.chartColors.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
|
||||
// --- Diagram Drawings (SmartArt fallback) ---
|
||||
if (/^ppt\/diagrams\/drawing\d+\.xml$/.test(normalizedPath)) {
|
||||
result.diagramDrawings.set(normalizedPath, await file.async('string'))
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user