import { prepareOdfPackage, type PreparedOdfPackage } from "./package"; import { parseFrameImage, parseTextBlocks, parseTextTable } from "./text"; import type { OdfParseOptions, OdfPresentationDocument, OdfPresentationShape, OdfPresentationShapeType, OdfPresentationSlide, } from "./types"; import { OdfParseError } from "./types"; import { NS, attribute, childElements, childrenNamed, firstChildNamed, parseLength, type XmlElement, } from "./xml"; export function parseOdp( input: ArrayBuffer, options: Omit = {}, ): OdfPresentationDocument { const prepared = prepareOdfPackage(input, { ...options, expectedFormat: "odp", }); return parsePreparedOdp(prepared); } export function parsePreparedOdp( prepared: PreparedOdfPackage, ): OdfPresentationDocument { if (prepared.format !== "odp") { throw new OdfParseError( "type-mismatch", "Prepared package is not an ODP document", ); } const body = firstChildNamed( prepared.content.documentElement!, NS.office, "body", ); const presentation = body && firstChildNamed(body, NS.office, "presentation"); if (!presentation) { throw new OdfParseError( "type-mismatch", "ODP content.xml does not contain an office:presentation document body", ); } const slides = childrenNamed(presentation, NS.draw, "page").map( (page, index) => parseSlide(page, index, prepared.context), ); return { format: "odp", mediaType: prepared.mediaType, metadata: prepared.metadata, styles: prepared.styles, assets: prepared.context.assets, warnings: prepared.context.warnings, pageWidth: prepared.pageWidth, pageHeight: prepared.pageHeight, slides, }; } function parseSlide( page: XmlElement, index: number, context: ReturnType["context"], ): OdfPresentationSlide { const notesElement = firstChildNamed(page, NS.presentation, "notes"); const shapes: OdfPresentationShape[] = []; let shapeIndex = 0; for (const element of childElements(page)) { if (element === notesElement || element.namespaceURI !== NS.draw) continue; shapes.push( parseShape(element, `slide-${index + 1}-shape-${++shapeIndex}`, context), ); } return { name: attribute(page, NS.draw, "name") ?? `Slide ${index + 1}`, styleName: attribute(page, NS.draw, "style-name"), masterPageName: attribute(page, NS.draw, "master-page-name"), layoutName: attribute( page, NS.presentation, "presentation-page-layout-name", ), shapes, notes: notesElement ? parseTextBlocks(notesElement, context) : [], }; } function parseShape( element: XmlElement, fallbackId: string, context: ReturnType["context"], ): OdfPresentationShape { const image = element.localName === "frame" ? parseFrameImage(element, context) : undefined; const textBox = firstChildNamed(element, NS.draw, "text-box"); const tableElement = firstChildNamed(element, NS.table, "table") ?? (textBox ? firstChildNamed(textBox, NS.table, "table") : undefined); const table = tableElement ? parseTextTable(tableElement, context) : undefined; const children: OdfPresentationShape[] = []; if (element.localName === "g") { let index = 0; for (const child of childElements(element)) { if (child.namespaceURI === NS.draw) { children.push(parseShape(child, `${fallbackId}-${++index}`, context)); } } } const blocks = textBox ? parseTextBlocks(textBox, context).filter( (block) => block.kind !== "table", ) : parseTextBlocks(element, context); return { id: attribute(element, NS.draw, "id") ?? fallbackId, type: shapeType( element, image !== undefined, table !== undefined, textBox !== undefined, ), name: attribute(element, NS.draw, "name"), styleName: attribute(element, NS.draw, "style-name"), textStyleName: attribute(element, NS.draw, "text-style-name"), layer: attribute(element, NS.draw, "layer"), x: parseLength(attribute(element, NS.svg, "x")), y: parseLength(attribute(element, NS.svg, "y")), width: parseLength(attribute(element, NS.svg, "width")), height: parseLength(attribute(element, NS.svg, "height")), transform: attribute(element, NS.draw, "transform"), blocks, image, table, children, }; } function shapeType( element: XmlElement, hasImage: boolean, hasTable: boolean, hasTextBox: boolean, ): OdfPresentationShapeType { if (hasImage) return "image"; if (hasTable) return "table"; if (hasTextBox) return "text-box"; if (element.localName === "rect") return "rectangle"; if (element.localName === "ellipse" || element.localName === "circle") return "ellipse"; if (element.localName === "line") return "line"; if (element.localName === "custom-shape") return "custom"; if (element.localName === "g") return "group"; return "unknown"; }