odf.js 1.3.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,116 @@
1
+ # odf.js
2
+
3
+ [![GitHub](https://img.shields.io/badge/GitHub-181717?logo=github&logoColor=white)](https://github.com/ExaDev/odf.js) [![npm](https://img.shields.io/badge/npm-CB3837?logo=npm&logoColor=white)](https://www.npmjs.com/package/odf.js) [![Release](https://img.shields.io/github/v/release/ExaDev/odf.js)](https://github.com/ExaDev/odf.js/releases/latest) [![CI](https://img.shields.io/github/actions/workflow/status/ExaDev/odf.js/ci.yml?branch=main)](https://github.com/ExaDev/odf.js/actions)
4
+
5
+ > A hand-written, dependency-minimal codec for the OpenDocument Format (ODF — OASIS/ISO 26300): `.odt`/`.ods`/`.odp`/`.odg`/`.odf`/`.odb`/`.odm` and their template variants, built on [Zod 4](https://zod.dev) codecs.
6
+
7
+ `odf.js` is the ODF sibling of [`ooxml.js`](https://github.com/ExaDev/ooxml.js), mirroring its architecture as closely as the two, structurally unrelated formats allow: a lossless ZIP-of-XML core that round-trips any package byte-for-content-faithful, with ergonomic typed readers layered on top for convenient access. Unlike OOXML — a ZIP of parts with a relationship-file (`.rels`) mechanism and an extension-defaults-plus-overrides `[Content_Types].xml` — ODF has no relationships at all (inter-part references are direct paths/IRIs) and an exhaustive `META-INF/manifest.xml` that enumerates every part explicitly. Where OOXML runs carry formatting directly as attributes, ODF has no inline/direct formatting whatsoever: every formatting difference, however small, must be a named "automatic style" — so `odf.js` owns a style-interning subsystem (`src/styles/`) with no equivalent anywhere in `ooxml.js`.
8
+
9
+ **This package does not depend on `ooxml.js`**, even though the two do near-identical jobs for their respective formats: `ooxml.js` is a package signed, SBOM-attested, and branded exclusively around ECMA-376/OOXML, so depending on it here would be a permanently wrong signal for an OASIS-standard codec, and would force a breaking `ooxml.js` release every time an ODF-only fix needed the shared primitive layer. Instead, `odf.js` duplicates the small (~400-line) generic ZIP/XML/`Package` layer as its own code — kept deliberately structurally identical (plain, unmarked shapes, no branding) so TypeScript's structural typing makes the two packages' `Package`/`XmlNode`/`XmlElement` values freely interchangeable wherever a shared consumer (like `documents.js`) needs to treat them uniformly, without either package formally depending on the other.
10
+
11
+ Both packages **do** depend on [`document-content-model`](https://github.com/ExaDev/document-content-model), the genuinely shared canonical schema for `ContentDocument`/`LayoutDocument` — the semantic content model (paragraphs, runs, tables, shapes, slides) both an ODF and an OOXML reader ultimately produce. `odf.js`'s typed readers return the real, imported `ContentSection`/`ContentSlide`/etc. types from that package, not a structurally-similar lookalike, so a downstream consumer (`documents.js`) can run an `.odt` through the exact same layout/pagination engine it already uses for `.docx`, unmodified.
12
+
13
+ ## Status
14
+
15
+ This package is under active development. What's built and shipped:
16
+
17
+ - **Lossless core** — generic ZIP-of-XML primitives (`Package`/`XmlNode`/`XmlElement`, XML parse/build, zip/unzip, base64, the `packageCodec`/`xmlCodec` `z.codec()` pairs) with zero ODF-specific knowledge.
18
+ - **Namespaces, media types, mimetype, manifest** (`src/ns.ts`, `src/media-type.ts`, `src/mimetype.ts`, `src/manifest.ts`) — full read *and* write, including `META-INF/manifest.xml`'s exhaustive per-part enumeration and the mimetype part's mandatory first-entry/stored/uncompressed byte layout, verified against real LibreOffice-produced output.
19
+ - **Style interning** (`src/styles/`) — `StyleRegistry`: adopts a part's existing automatic styles on construction, finds-or-mints on `intern()`, fingerprints on canonical serialized properties plus parent style name (never `JSON.stringify`), and is collision-checked across all four style containers a document can have.
20
+ - **Shared typed primitives** (`src/typed/shared/`) — ODF length-unit parsing (`cm`/`mm`/`in`/`pt`/`pc`/`px`), A1-style spreadsheet cell-reference computation with repeat-count cursor advancement, colour/geometry parsing into `document-content-model`'s own types, ODF's `text:s`/`text:tab`/`text:line-break` whitespace-run decoding, the read-side style cascade (`style:default-style` → parent chain → the referenced style — one layer shorter than OOXML's, since ODF has no separate direct-formatting layer on top), and `meta.xml` reading.
21
+
22
+ Not yet built: the per-format typed readers (`readOdt`, `readOds`, `readOdp`, `readOdg`, `readOdfFormula`, `readOdbInventory`), live-view editors, and the `.odm` master-document/`.odb` database-table-export subsystems. This section will be replaced with real usage examples once those land — see the [Architecture](#architecture) section below for the intended shape, and this repository's own commit history/releases for current progress.
23
+
24
+ ## Getting started
25
+
26
+ Requires Node.js `>=20` and pnpm `11.6.0` (pinned via `packageManager` in `package.json`).
27
+
28
+ ```sh
29
+ pnpm install
30
+ ```
31
+
32
+ Install as a dependency in another project:
33
+
34
+ ```sh
35
+ pnpm add odf.js
36
+ # or
37
+ npm install odf.js
38
+ ```
39
+
40
+ ## Usage
41
+
42
+ The lossless core — the only public surface stable enough to document with real examples right now:
43
+
44
+ ```ts
45
+ import { decodePackage, encodePackage } from 'odf.js';
46
+
47
+ // .odt / .ods / .odp bytes -> faithful JSON Package
48
+ const pkg = decodePackage(new Uint8Array(await file.arrayBuffer()));
49
+
50
+ // ...inspect pkg.parts...
51
+
52
+ // Package -> bytes (content-identical, mimetype-first/stored, manifest untouched)
53
+ const bytes = encodePackage(pkg);
54
+ ```
55
+
56
+ Manifest and mimetype, ODF's own package-identity mechanism (no relationships, unlike OOXML):
57
+
58
+ ```ts
59
+ import { readManifest, syncManifest, setDocumentMediaType, readMimetype } from 'odf.js';
60
+
61
+ const manifest = readManifest(pkg); // { entries: [{ fullPath, mediaType }, ...] }
62
+ setDocumentMediaType(pkg, 'application/vnd.oasis.opendocument.text'); // updates mimetype + manifest root entry atomically
63
+ syncManifest(pkg); // rebuilds manifest.xml to exactly match pkg's current parts
64
+ readMimetype(pkg); // 'application/vnd.oasis.opendocument.text'
65
+ ```
66
+
67
+ ## Architecture
68
+
69
+ Layered from a lossless core outward, mirroring `ooxml.js`'s own structure:
70
+
71
+ - **`src/model/`** — `Package`/`XmlNode`/`XmlElement` and friends: a duplicate-by-design copy of `ooxml.js`'s equivalent, kept structurally identical (see [Why no `ooxml.js` dependency](#why-no-ooxmljs-dependency) above).
72
+ - **`src/xml/`** — `parse.ts`/`build.ts` (XML string ⇄ `XmlNode[]` forest via `fast-xml-parser`), `fragment.ts`/`entities.ts` (production element/text-node construction and entity encoding — `odf.js` writes `manifest.xml` itself, unlike `ooxml.js`'s read-only stance on OPC relationships, so this needs to be real writing code, not test-only scaffolding), `query.ts` (shared tree-query helpers).
73
+ - **`src/zip.ts`** — takes *ordered* `[path, entry]` tuples, not a `Record`, specifically so ODF's mimetype-first/stored/uncompressed requirement doesn't depend on `Record`/`Object.keys` insertion order surviving a Zod round trip.
74
+ - **`src/package-io/`** — `write.ts` hoists a `mimetype` part first (stored) and `META-INF/manifest.xml` second, if present, before everything else in existing order — the one deliberate behavioural difference from `ooxml.js`'s own writer, and never fabricates either part as a side effect.
75
+ - **`src/manifest.ts`** — unlike `ooxml.js` (which only ever *reads* OPC relationships, leaving writing to `documents.js`), `odf.js` owns manifest read **and** write, since the manifest is ODF's one mandatory part and its correctness is exhaustive.
76
+ - **`src/styles/`** — `properties.ts` (the property-bag shape + real ODF attribute parsing), `serialize.ts` (canonical, deterministic property-bag → XML attributes), `registry.ts` (`StyleRegistry`, ODF's mandatory style-interning layer, no OOXML equivalent), `span.ts` (character-range wrapping into a formattable `text:span`, correctly splitting `text:s`/`text:tab` elements that straddle a boundary).
77
+ - **`src/typed/shared/`** — the ODF-specific typed primitives every future format reader builds on: `units.ts`, `a1.ts`, `color.ts`/`geometry.ts` (parsing into `document-content-model`'s own types, never redefining them), `style.ts` (a thin re-export — ODF's style-properties concern is fully covered by `styles/properties.ts` and the cascade below), `text.ts` (whitespace-run decoding), `cascade.ts` (the read-side style-resolution walk), `metadata.ts` (`meta.xml` reading).
78
+ - **`src/typed/odt/`, `src/typed/ods.ts`, `src/typed/odp/`, `src/typed/odg/`, `src/typed/draw/`, `src/typed/formula/`** — per-format typed readers, in progress (see [Status](#status)).
79
+
80
+ ## Why no `ooxml.js` dependency
81
+
82
+ See the top of this README — the short version: `ooxml.js`'s branding and signed SBOM make it the wrong dependency for an OASIS-standard package regardless of how much low-level code the two could share; `document-content-model` is the neutral package both actually depend on for the parts that are genuinely, permanently identical (the semantic content vocabulary), while the ZIP-of-XML primitive layer stays duplicated on purpose.
83
+
84
+ ## Conventions
85
+
86
+ - **Zod-first schema/type/guard**, matching `ooxml.js`/`document-content-model`: every model type is inferred from its Zod schema, never hand-written.
87
+ - **Recursive types use a hand-written structural guard, not `z.lazy`** — the same `z.lazy`-collapses-to-`unknown` issue `ooxml.js`'s `XmlNode` and `document-content-model`'s `ContentBlock` already work around.
88
+ - **No type assertions anywhere** — `assertionStyle: 'never'`, `noInlineConfig: true`, matching both sibling packages exactly.
89
+ - **Ground truth over memory for every ODF spec fact.** Namespace URIs, media types, style-property attribute names, and `meta.xml` element names are all verified against either the live OASIS ODF specification or real files produced by an installed LibreOffice, never assumed from pattern-matching an OOXML analogue or a remembered convention — several confirmed traps exist specifically because the "obvious" guess is wrong (see [Gotchas](#gotchas-and-quirks)).
90
+
91
+ ## Gotchas and quirks
92
+
93
+ - **Several ODF namespace URIs are not what you'd guess from the prefix.** `draw:` is `...xmlns:drawing:1.0`, not `...draw:1.0`; `number:` is `...xmlns:datastyle:1.0`, not `...number:1.0`; `fo:`/`svg:`/`smil:` are OASIS's own `*-compatible:1.0` URIs, not the real W3C namespaces those prefixes suggest. See `src/ns.ts`'s inline comments for the full, verified table.
94
+ - **`.odb`'s real media type is `application/vnd.oasis.opendocument.base`**, not `...database` — a common stale/wrong value found in some third-party documentation.
95
+ - **ODF's `dc:creator` is not "the author."** It records whoever most recently *saved* the document (Dublin Core's own definition); the original author is `meta:initial-creator`. `typed/shared/metadata.ts` maps `LayoutMetadata.author` to `meta:initial-creator`, matching the byline role `ooxml.js`'s own `DocumentMetadata.author` plays for OOXML.
96
+ - **`meta:keyword` appears once per keyword**, unlike OOXML's single comma-separated `cp:keywords` element.
97
+ - **`table:number-columns-repeated`/`table:number-rows-repeated` must be cursor-advanced, never materialized.** A real spreadsheet has trailing cells/rows with repeat counts over a million; `typed/shared/a1.ts`'s cursor advances in O(1) without allocating that many objects — tested against a real repeat count taken from a genuine LibreOffice template.
98
+ - **ODF cells carry no explicit cell-reference attribute at all** (unlike xlsx's `r="B7"`) — `typed/shared/a1.ts` computes A1-style references from a running column/row cursor as a reader walks cells in document order.
99
+
100
+ ## Release and publishing
101
+
102
+ `.github/workflows/ci.yml` runs commitlint, lint, typecheck, the unit suite, and the smoke test on every push and pull request. On a push to `main` where those all pass, `release.config.ts` drives [semantic-release](https://semantic-release.gitbook.io/semantic-release): commit history since the last tag decides the version bump, `CHANGELOG.md` and `package.json` are committed back to `main`, a GitHub Release is cut, and the package publishes to [npmjs.org](https://www.npmjs.com/package/odf.js) — via npm's OIDC trusted publishing, so no `NPM_TOKEN` exists anywhere in the pipeline. A further job republishes the same build under the scoped `@exadev/odf.js` alias to GitHub Packages, and another signs an SPDX SBOM and build-provenance attestation against the exact release tarball.
103
+
104
+ ## Contributing
105
+
106
+ Commits follow Conventional Commits (`feat:`, `fix:`, `test:`, `chore:`, …), enforced by commitlint via a husky `commit-msg` hook and a CI `commitlint` job. A husky `pre-commit` hook runs `lint-staged` (`eslint --fix` on staged `*.ts` files) and `pre-push` runs the test suite. There is a single `main` branch and no open pull request workflow established so far.
107
+
108
+ ## References
109
+
110
+ - [ooxml.js](https://github.com/ExaDev/ooxml.js) — the sibling package doing the equivalent lossless-codec job for OOXML (docx/pptx/xlsx). Architecturally mirrored, deliberately not depended on — see [Why no `ooxml.js` dependency](#why-no-ooxmljs-dependency).
111
+ - [document-content-model](https://github.com/ExaDev/document-content-model) — the canonical `ContentDocument`/`LayoutDocument` schema pivot both this package and `ooxml.js` depend on.
112
+ - [documents.js](https://github.com/ExaDev/documents.js) — the intended downstream consumer, adding ODF ⇄ PDF conversion and a live-view ODF editor once this package's typed readers land.
113
+
114
+ ## License
115
+
116
+ MIT
package/dist/index.cjs CHANGED
@@ -1243,6 +1243,17 @@ function childrenWithTag(element, tag) {
1243
1243
  for (const child of element.children) if (child.type === "element" && child.tag === tag) out.push(child);
1244
1244
  return out;
1245
1245
  }
1246
+ function* walk(nodes) {
1247
+ for (const node of nodes) {
1248
+ yield node;
1249
+ if (node.type === "element") yield* walk(node.children);
1250
+ }
1251
+ }
1252
+ function elementsWithTag(nodes, tag) {
1253
+ const out = [];
1254
+ for (const node of walk(nodes)) if (node.type === "element" && node.tag === tag) out.push(node);
1255
+ return out;
1256
+ }
1246
1257
  function attrValue(element, name) {
1247
1258
  return element.attributes.find((attribute) => attribute.name === name)?.value;
1248
1259
  }
@@ -1522,13 +1533,14 @@ function collectStyles(pkg) {
1522
1533
  defaultByFamily
1523
1534
  };
1524
1535
  }
1525
- function resolveStyle(styleName, family, pkg) {
1536
+ function resolveStyleElementChain(styleName, family, pkg) {
1526
1537
  const { byName, defaultByFamily } = collectStyles(pkg);
1527
1538
  const diagnostics = [];
1539
+ const elements = [];
1528
1540
  const defaultElement = defaultByFamily.get(family);
1529
- let properties = defaultElement === void 0 ? {} : parseStyleElementProperties(defaultElement).properties;
1541
+ if (defaultElement !== void 0) elements.push(defaultElement);
1530
1542
  if (styleName === void 0) return {
1531
- properties,
1543
+ elements,
1532
1544
  diagnostics
1533
1545
  };
1534
1546
  const chain = [];
@@ -1556,7 +1568,16 @@ function resolveStyle(styleName, family, pkg) {
1556
1568
  currentName = attrValue(element, "style:parent-style-name");
1557
1569
  }
1558
1570
  chain.reverse();
1559
- for (const element of chain) properties = {
1571
+ elements.push(...chain);
1572
+ return {
1573
+ elements,
1574
+ diagnostics
1575
+ };
1576
+ }
1577
+ function resolveStyle(styleName, family, pkg) {
1578
+ const { elements, diagnostics } = resolveStyleElementChain(styleName, family, pkg);
1579
+ let properties = {};
1580
+ for (const element of elements) properties = {
1560
1581
  ...properties,
1561
1582
  ...parseStyleElementProperties(element).properties
1562
1583
  };
@@ -1565,6 +1586,10 @@ function resolveStyle(styleName, family, pkg) {
1565
1586
  diagnostics
1566
1587
  };
1567
1588
  }
1589
+ function findStyleElement(styleName, family, pkg) {
1590
+ const { byName } = collectStyles(pkg);
1591
+ return byName.get(nameKey(family, styleName));
1592
+ }
1568
1593
  //#endregion
1569
1594
  //#region src/typed/shared/metadata.ts
1570
1595
  const META_PART = "meta.xml";
@@ -1603,6 +1628,376 @@ function readOdfMetadata(pkg) {
1603
1628
  return metadata;
1604
1629
  }
1605
1630
  //#endregion
1631
+ //#region src/typed/shared/paragraph.ts
1632
+ function collectRuns(nodes, baseProperties, pkg, out) {
1633
+ for (const node of nodes) {
1634
+ if (node.type === "text") {
1635
+ if (node.value.length > 0) out.push(runFromText(decodeXmlText(node.value), baseProperties));
1636
+ continue;
1637
+ }
1638
+ if (node.type !== "element") continue;
1639
+ if (node.tag === "text:s") out.push(runFromText(" ".repeat(getOdfSpaceCount(node)), baseProperties));
1640
+ else if (node.tag === "text:tab") out.push(runFromText(" ", baseProperties));
1641
+ else if (node.tag === "text:line-break") out.push(runFromText("\n", baseProperties));
1642
+ else if (node.tag === "text:span") {
1643
+ const styleName = attrValue(node, "text:style-name");
1644
+ const spanProperties = {
1645
+ ...baseProperties,
1646
+ ...resolveStyle(styleName, "text", pkg).properties
1647
+ };
1648
+ collectRuns(node.children, spanProperties, pkg, out);
1649
+ }
1650
+ }
1651
+ }
1652
+ function runFromText(text, properties) {
1653
+ return {
1654
+ text,
1655
+ bold: properties.bold,
1656
+ italic: properties.italic,
1657
+ underline: properties.underline,
1658
+ strike: properties.strike,
1659
+ fontFamily: properties.fontFamily,
1660
+ sizePt: properties.sizePt,
1661
+ color: properties.color
1662
+ };
1663
+ }
1664
+ function readOdfParagraph(pElement, pkg) {
1665
+ const styleName = attrValue(pElement, "text:style-name");
1666
+ const paragraphProperties = resolveStyle(styleName, "paragraph", pkg).properties;
1667
+ const runs = [];
1668
+ collectRuns(pElement.children, paragraphProperties, pkg, runs);
1669
+ return {
1670
+ kind: "paragraph",
1671
+ runs,
1672
+ styleId: styleName,
1673
+ alignment: paragraphProperties.alignment,
1674
+ spacingBeforePt: paragraphProperties.spacingBeforePt,
1675
+ spacingAfterPt: paragraphProperties.spacingAfterPt,
1676
+ lineSpacing: paragraphProperties.lineSpacing,
1677
+ indentLeftPt: paragraphProperties.indentLeftPt,
1678
+ indentFirstLinePt: paragraphProperties.indentFirstLinePt
1679
+ };
1680
+ }
1681
+ //#endregion
1682
+ //#region src/typed/shared/table.ts
1683
+ function readRepeatCount(element, attrName) {
1684
+ const raw = attrValue(element, attrName);
1685
+ if (raw === void 0) return 1;
1686
+ const parsed = Number.parseInt(raw, 10);
1687
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : 1;
1688
+ }
1689
+ function resolveColumnWidthPt(columnElement, pkg) {
1690
+ const styleName = attrValue(columnElement, "table:style-name");
1691
+ const styleElement = styleName === void 0 ? void 0 : findStyleElement(styleName, "table-column", pkg);
1692
+ const props = styleElement === void 0 ? void 0 : childrenWithTag(styleElement, "style:table-column-properties")[0];
1693
+ const widthValue = props === void 0 ? void 0 : attrValue(props, "style:column-width");
1694
+ return widthValue === void 0 ? 0 : parseOdfLength(widthValue) ?? 0;
1695
+ }
1696
+ function resolveRowHeightPt(rowElement, pkg) {
1697
+ const styleName = attrValue(rowElement, "table:style-name");
1698
+ const styleElement = styleName === void 0 ? void 0 : findStyleElement(styleName, "table-row", pkg);
1699
+ const props = styleElement === void 0 ? void 0 : childrenWithTag(styleElement, "style:table-row-properties")[0];
1700
+ const heightValue = props === void 0 ? void 0 : attrValue(props, "style:row-height");
1701
+ return heightValue === void 0 ? void 0 : parseOdfLength(heightValue);
1702
+ }
1703
+ function readTableCellBackground(cellElement, pkg) {
1704
+ const styleName = attrValue(cellElement, "table:style-name");
1705
+ const styleElement = styleName === void 0 ? void 0 : findStyleElement(styleName, "table-cell", pkg);
1706
+ const props = styleElement === void 0 ? void 0 : childrenWithTag(styleElement, "style:table-cell-properties")[0];
1707
+ const value = props === void 0 ? void 0 : attrValue(props, "fo:background-color");
1708
+ return value === void 0 ? void 0 : parseOdfColor(value);
1709
+ }
1710
+ function readTableCell(cellElement, pkg) {
1711
+ const blocks = childrenWithTag(cellElement, "text:p").map((p) => readOdfParagraph(p, pkg));
1712
+ const colSpanRaw = attrValue(cellElement, "table:number-columns-spanned");
1713
+ const rowSpanRaw = attrValue(cellElement, "table:number-rows-spanned");
1714
+ return {
1715
+ blocks,
1716
+ colSpan: colSpanRaw === void 0 ? void 0 : Number.parseInt(colSpanRaw, 10),
1717
+ rowSpan: rowSpanRaw === void 0 ? void 0 : Number.parseInt(rowSpanRaw, 10),
1718
+ background: readTableCellBackground(cellElement, pkg)
1719
+ };
1720
+ }
1721
+ function readTableRow(rowElement, pkg) {
1722
+ const cells = [];
1723
+ for (const child of rowElement.children) {
1724
+ if (child.type !== "element") continue;
1725
+ if (child.tag === "table:covered-table-cell") {
1726
+ const repeat = readRepeatCount(child, "table:number-columns-repeated");
1727
+ for (let i = 0; i < repeat; i++) cells.push({ blocks: [] });
1728
+ } else if (child.tag === "table:table-cell") {
1729
+ const cell = readTableCell(child, pkg);
1730
+ const repeat = readRepeatCount(child, "table:number-columns-repeated");
1731
+ for (let i = 0; i < repeat; i++) cells.push(cell);
1732
+ }
1733
+ }
1734
+ return {
1735
+ cells,
1736
+ heightPt: resolveRowHeightPt(rowElement, pkg)
1737
+ };
1738
+ }
1739
+ function readOdfTable(tableElement, pkg) {
1740
+ const columnWidthsPt = [];
1741
+ for (const column of childrenWithTag(tableElement, "table:table-column")) {
1742
+ const widthPt = resolveColumnWidthPt(column, pkg);
1743
+ const repeat = readRepeatCount(column, "table:number-columns-repeated");
1744
+ for (let i = 0; i < repeat; i++) columnWidthsPt.push(widthPt);
1745
+ }
1746
+ const rows = [];
1747
+ for (const rowElement of childrenWithTag(tableElement, "table:table-row")) {
1748
+ const row = readTableRow(rowElement, pkg);
1749
+ const repeat = readRepeatCount(rowElement, "table:number-rows-repeated");
1750
+ for (let i = 0; i < repeat; i++) rows.push(row);
1751
+ }
1752
+ return {
1753
+ kind: "table",
1754
+ rows,
1755
+ columnWidthsPt
1756
+ };
1757
+ }
1758
+ //#endregion
1759
+ //#region src/typed/shared/transform.ts
1760
+ const FUNCTION_PATTERN = /([a-zA-Z]+)\s*\(\s*([^)]*?)\s*\)/g;
1761
+ function parseOdfTransform(value) {
1762
+ const functions = [];
1763
+ for (const match of value.matchAll(FUNCTION_PATTERN)) {
1764
+ const name = match[1];
1765
+ const argsRaw = match[2];
1766
+ if (name === void 0 || argsRaw === void 0) continue;
1767
+ const args = argsRaw.split(/\s+/).filter((arg) => arg.length > 0);
1768
+ if (name === "rotate") {
1769
+ const angleArg = args[0];
1770
+ if (angleArg === void 0) continue;
1771
+ const angleRad = Number(angleArg);
1772
+ if (!Number.isFinite(angleRad)) continue;
1773
+ functions.push({
1774
+ kind: "rotate",
1775
+ angleRad
1776
+ });
1777
+ } else if (name === "translate") {
1778
+ const xArg = args[0];
1779
+ if (xArg === void 0) continue;
1780
+ const yArg = args[1];
1781
+ const xPt = parseOdfLength(xArg);
1782
+ const yPt = yArg === void 0 ? 0 : parseOdfLength(yArg);
1783
+ if (xPt === void 0 || yPt === void 0) continue;
1784
+ functions.push({
1785
+ kind: "translate",
1786
+ xPt,
1787
+ yPt
1788
+ });
1789
+ }
1790
+ }
1791
+ return functions;
1792
+ }
1793
+ function applyOdfTransform(functions, point) {
1794
+ let current = point;
1795
+ for (const fn of functions) if (fn.kind === "rotate") {
1796
+ const cos = Math.cos(fn.angleRad);
1797
+ const sin = Math.sin(fn.angleRad);
1798
+ current = {
1799
+ xPt: current.xPt * cos + current.yPt * sin,
1800
+ yPt: current.yPt * cos - current.xPt * sin
1801
+ };
1802
+ } else current = {
1803
+ xPt: current.xPt + fn.xPt,
1804
+ yPt: current.yPt + fn.yPt
1805
+ };
1806
+ return current;
1807
+ }
1808
+ function netRotationDeg(functions) {
1809
+ let totalRad = 0;
1810
+ for (const fn of functions) if (fn.kind === "rotate") totalRad += fn.angleRad;
1811
+ return -totalRad * 180 / Math.PI;
1812
+ }
1813
+ function resolveOdfShapeGeometry(element) {
1814
+ const transformValue = attrValue(element, "draw:transform");
1815
+ if (transformValue === void 0) {
1816
+ const box = parseBox(element);
1817
+ return box === void 0 ? void 0 : {
1818
+ frame: box,
1819
+ rotationDeg: void 0
1820
+ };
1821
+ }
1822
+ const widthValue = attrValue(element, "svg:width");
1823
+ const heightValue = attrValue(element, "svg:height");
1824
+ if (widthValue === void 0 || heightValue === void 0) return;
1825
+ const widthPt = parseOdfLength(widthValue);
1826
+ const heightPt = parseOdfLength(heightValue);
1827
+ if (widthPt === void 0 || heightPt === void 0) return;
1828
+ const functions = parseOdfTransform(transformValue);
1829
+ const center = applyOdfTransform(functions, {
1830
+ xPt: widthPt / 2,
1831
+ yPt: heightPt / 2
1832
+ });
1833
+ const rotationDeg = netRotationDeg(functions);
1834
+ return {
1835
+ frame: {
1836
+ xPt: center.xPt - widthPt / 2,
1837
+ yPt: center.yPt - heightPt / 2,
1838
+ widthPt,
1839
+ heightPt
1840
+ },
1841
+ rotationDeg: rotationDeg === 0 ? void 0 : rotationDeg
1842
+ };
1843
+ }
1844
+ function composeOdfGroupTransform(groupFunctions, child) {
1845
+ if (groupFunctions.length === 0) return child;
1846
+ const newCenter = applyOdfTransform(groupFunctions, {
1847
+ xPt: child.frame.xPt + child.frame.widthPt / 2,
1848
+ yPt: child.frame.yPt + child.frame.heightPt / 2
1849
+ });
1850
+ const newRotationDeg = (child.rotationDeg ?? 0) + netRotationDeg(groupFunctions);
1851
+ return {
1852
+ frame: {
1853
+ xPt: newCenter.xPt - child.frame.widthPt / 2,
1854
+ yPt: newCenter.yPt - child.frame.heightPt / 2,
1855
+ widthPt: child.frame.widthPt,
1856
+ heightPt: child.frame.heightPt
1857
+ },
1858
+ rotationDeg: newRotationDeg === 0 ? void 0 : newRotationDeg
1859
+ };
1860
+ }
1861
+ //#endregion
1862
+ //#region src/typed/draw/shapes.ts
1863
+ const ZERO_INSETS = {
1864
+ insetLeftPt: 0,
1865
+ insetTopPt: 0,
1866
+ insetRightPt: 0,
1867
+ insetBottomPt: 0
1868
+ };
1869
+ function readPaddingPt(props, attrName) {
1870
+ const value = attrValue(props, attrName);
1871
+ return value === void 0 ? void 0 : parseOdfLength(value);
1872
+ }
1873
+ function readFrameInsets(frame, pkg) {
1874
+ const { elements } = resolveStyleElementChain(attrValue(frame, "draw:style-name"), "graphic", pkg);
1875
+ let insets = ZERO_INSETS;
1876
+ for (const element of elements) {
1877
+ const props = childrenWithTag(element, "style:graphic-properties")[0];
1878
+ if (props === void 0) continue;
1879
+ insets = {
1880
+ insetLeftPt: readPaddingPt(props, "fo:padding-left") ?? insets.insetLeftPt,
1881
+ insetTopPt: readPaddingPt(props, "fo:padding-top") ?? insets.insetTopPt,
1882
+ insetRightPt: readPaddingPt(props, "fo:padding-right") ?? insets.insetRightPt,
1883
+ insetBottomPt: readPaddingPt(props, "fo:padding-bottom") ?? insets.insetBottomPt
1884
+ };
1885
+ }
1886
+ return insets;
1887
+ }
1888
+ function readDrawImageBlock(image, frameBox, pkg) {
1889
+ const href = attrValue(image, "xlink:href");
1890
+ const part = href === void 0 ? void 0 : pkg.parts[href];
1891
+ if (part?.kind !== "binary") return;
1892
+ const format = sniffImageFormat(base64ToBytes(part.base64));
1893
+ if (format === void 0) return;
1894
+ return {
1895
+ kind: "image",
1896
+ format,
1897
+ base64: part.base64,
1898
+ widthPt: frameBox.widthPt,
1899
+ heightPt: frameBox.heightPt
1900
+ };
1901
+ }
1902
+ function readDrawFrameContent(frame, frameBox, pkg) {
1903
+ const table = childrenWithTag(frame, "table:table")[0];
1904
+ if (table !== void 0) return [readOdfTable(table, pkg)];
1905
+ const textBox = childrenWithTag(frame, "draw:text-box")[0];
1906
+ if (textBox !== void 0) return elementsWithTag(textBox.children, "text:p").map((p) => readOdfParagraph(p, pkg));
1907
+ const image = childrenWithTag(frame, "draw:image")[0];
1908
+ if (image !== void 0) {
1909
+ const block = readDrawImageBlock(image, frameBox, pkg);
1910
+ return block === void 0 ? [] : [block];
1911
+ }
1912
+ return [];
1913
+ }
1914
+ function readDrawFrame(frame, groupFunctions, pkg) {
1915
+ const ownGeometry = resolveOdfShapeGeometry(frame);
1916
+ if (ownGeometry === void 0) return;
1917
+ const geometry = composeOdfGroupTransform(groupFunctions, ownGeometry);
1918
+ return {
1919
+ name: attrValue(frame, "draw:name"),
1920
+ frame: geometry.frame,
1921
+ rotationDeg: geometry.rotationDeg,
1922
+ ...readFrameInsets(frame, pkg),
1923
+ blocks: readDrawFrameContent(frame, geometry.frame, pkg)
1924
+ };
1925
+ }
1926
+ function readOwnTransformFunctions(element) {
1927
+ const value = attrValue(element, "draw:transform");
1928
+ return value === void 0 ? [] : parseOdfTransform(value);
1929
+ }
1930
+ function walkDrawShapes(children, groupFunctions, pkg, out) {
1931
+ for (const node of children) {
1932
+ if (node.type !== "element") continue;
1933
+ if (node.tag === "draw:frame") {
1934
+ const shape = readDrawFrame(node, groupFunctions, pkg);
1935
+ if (shape !== void 0) out.push(shape);
1936
+ } else if (node.tag === "draw:g") {
1937
+ const ownFunctions = readOwnTransformFunctions(node);
1938
+ const nested = ownFunctions.length === 0 ? groupFunctions : [...ownFunctions, ...groupFunctions];
1939
+ walkDrawShapes(node.children, nested, pkg, out);
1940
+ }
1941
+ }
1942
+ }
1943
+ //#endregion
1944
+ //#region src/typed/odp/read.ts
1945
+ const CONTENT_PART = "content.xml";
1946
+ const STYLES_PART = "styles.xml";
1947
+ const AUTOMATIC_STYLE_PARTS = [CONTENT_PART, STYLES_PART];
1948
+ function findMasterPageElement(pkg, masterPageName) {
1949
+ if (masterPageName === void 0) return;
1950
+ const stylesPart = pkg.parts[STYLES_PART];
1951
+ if (stylesPart?.kind !== "xml") return;
1952
+ const root = rootElement(stylesPart.nodes);
1953
+ const masterStyles = root === void 0 ? void 0 : findChildElement(root.children, "office:master-styles");
1954
+ if (masterStyles === void 0) return;
1955
+ return childrenWithTag(masterStyles, "style:master-page").find((element) => attrValue(element, "style:name") === masterPageName);
1956
+ }
1957
+ function findPageLayoutElement(pkg, pageLayoutName) {
1958
+ if (pageLayoutName === void 0) return;
1959
+ for (const partPath of AUTOMATIC_STYLE_PARTS) {
1960
+ const part = pkg.parts[partPath];
1961
+ if (part?.kind !== "xml") continue;
1962
+ const root = rootElement(part.nodes);
1963
+ const automaticStyles = root === void 0 ? void 0 : findChildElement(root.children, "office:automatic-styles");
1964
+ if (automaticStyles === void 0) continue;
1965
+ const found = childrenWithTag(automaticStyles, "style:page-layout").find((element) => attrValue(element, "style:name") === pageLayoutName);
1966
+ if (found !== void 0) return found;
1967
+ }
1968
+ }
1969
+ function readSlideSize(page, pkg) {
1970
+ const masterPage = findMasterPageElement(pkg, attrValue(page, "draw:master-page-name"));
1971
+ const pageLayout = findPageLayoutElement(pkg, masterPage === void 0 ? void 0 : attrValue(masterPage, "style:page-layout-name"));
1972
+ const properties = pageLayout === void 0 ? void 0 : childrenWithTag(pageLayout, "style:page-layout-properties")[0];
1973
+ return (properties === void 0 ? void 0 : parsePageSize(properties)) ?? document_content_model.SLIDE_SIZE_WIDESCREEN;
1974
+ }
1975
+ function readSlideNotes(page) {
1976
+ const notes = childrenWithTag(page, "presentation:notes")[0];
1977
+ if (notes === void 0) return "";
1978
+ return elementsWithTag(notes.children, "text:p").map(decodeOdfText).join("\n");
1979
+ }
1980
+ function readSlide(page, pkg) {
1981
+ const shapes = [];
1982
+ walkDrawShapes(page.children, [], pkg, shapes);
1983
+ return {
1984
+ size: readSlideSize(page, pkg),
1985
+ shapes,
1986
+ notes: readSlideNotes(page)
1987
+ };
1988
+ }
1989
+ function readOdp(pkg) {
1990
+ const contentPart = pkg.parts[CONTENT_PART];
1991
+ const root = contentPart?.kind === "xml" ? rootElement(contentPart.nodes) : void 0;
1992
+ const body = root === void 0 ? void 0 : findChildElement(root.children, "office:body");
1993
+ const presentation = body === void 0 ? void 0 : findChildElement(body.children, "office:presentation");
1994
+ const pages = presentation === void 0 ? [] : childrenWithTag(presentation, "draw:page");
1995
+ return {
1996
+ metadata: readOdfMetadata(pkg),
1997
+ slides: pages.map((page) => readSlide(page, pkg))
1998
+ };
1999
+ }
2000
+ //#endregion
1606
2001
  Object.defineProperty(exports, "AlignmentSchema", {
1607
2002
  enumerable: true,
1608
2003
  get: function() {
@@ -1633,6 +2028,7 @@ exports.XmlNodeSchema = XmlNodeSchema;
1633
2028
  exports.XmlPartSchema = XmlPartSchema;
1634
2029
  exports.XmlPiSchema = XmlPiSchema;
1635
2030
  exports.XmlTextSchema = XmlTextSchema;
2031
+ exports.applyOdfTransform = applyOdfTransform;
1636
2032
  exports.attrValue = attrValue;
1637
2033
  exports.base64ToBytes = base64ToBytes;
1638
2034
  exports.buildManifest = buildManifest;
@@ -1643,14 +2039,17 @@ exports.canonicalPropertiesString = canonicalPropertiesString;
1643
2039
  exports.cellReference = cellReference;
1644
2040
  exports.childrenWithTag = childrenWithTag;
1645
2041
  exports.columnIndexToLetters = columnIndexToLetters;
2042
+ exports.composeOdfGroupTransform = composeOdfGroupTransform;
1646
2043
  exports.decodeOdfText = decodeOdfText;
1647
2044
  exports.decodePackage = decodePackage;
1648
2045
  exports.decodeXmlText = decodeXmlText;
1649
2046
  exports.el = el;
2047
+ exports.elementsWithTag = elementsWithTag;
1650
2048
  exports.encodePackage = encodePackage;
1651
2049
  exports.encodeXmlText = encodeXmlText;
1652
2050
  exports.ensureSpan = ensureSpan;
1653
2051
  exports.findChildElement = findChildElement;
2052
+ exports.findStyleElement = findStyleElement;
1654
2053
  exports.formatOdfColor = formatOdfColor;
1655
2054
  exports.formatOdfLength = formatOdfLength;
1656
2055
  exports.formatPercentageMultiplier = formatPercentageMultiplier;
@@ -1660,6 +2059,7 @@ exports.isStyleFamily = isStyleFamily;
1660
2059
  exports.isXmlNode = isXmlNode;
1661
2060
  exports.measureOdfNodeLength = measureOdfNodeLength;
1662
2061
  exports.mediaTypeForExtension = mediaTypeForExtension;
2062
+ exports.netRotationDeg = netRotationDeg;
1663
2063
  exports.packageCodec = packageCodec;
1664
2064
  exports.paragraphPropertiesToAttributes = paragraphPropertiesToAttributes;
1665
2065
  exports.parseBox = parseBox;
@@ -1667,16 +2067,23 @@ exports.parseLength = parseLength;
1667
2067
  exports.parseMargins = parseMargins;
1668
2068
  exports.parseOdfColor = parseOdfColor;
1669
2069
  exports.parseOdfLength = parseOdfLength;
2070
+ exports.parseOdfTransform = parseOdfTransform;
1670
2071
  exports.parsePackage = parsePackage;
1671
2072
  exports.parsePageSize = parsePageSize;
1672
2073
  exports.parseParagraphProperties = parseParagraphProperties;
1673
2074
  exports.parseStyleElementProperties = parseStyleElementProperties;
1674
2075
  exports.parseTextProperties = parseTextProperties;
1675
2076
  exports.parseXml = parseXml;
2077
+ exports.readDrawFrame = readDrawFrame;
1676
2078
  exports.readManifest = readManifest;
1677
2079
  exports.readMimetype = readMimetype;
1678
2080
  exports.readOdfMetadata = readOdfMetadata;
2081
+ exports.readOdfParagraph = readOdfParagraph;
2082
+ exports.readOdfTable = readOdfTable;
2083
+ exports.readOdp = readOdp;
2084
+ exports.resolveOdfShapeGeometry = resolveOdfShapeGeometry;
1679
2085
  exports.resolveStyle = resolveStyle;
2086
+ exports.resolveStyleElementChain = resolveStyleElementChain;
1680
2087
  exports.rootElement = rootElement;
1681
2088
  exports.serializePackage = serializePackage;
1682
2089
  exports.setDocumentMediaType = setDocumentMediaType;
@@ -1687,6 +2094,7 @@ exports.textPropertiesToAttributes = textPropertiesToAttributes;
1687
2094
  exports.txt = txt;
1688
2095
  exports.unzipPackage = unzipPackage;
1689
2096
  exports.validateManifest = validateManifest;
2097
+ exports.walkDrawShapes = walkDrawShapes;
1690
2098
  exports.writeManifest = writeManifest;
1691
2099
  exports.writeMimetype = writeMimetype;
1692
2100
  exports.xmlCodec = xmlCodec;
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { Alignment, AlignmentSchema, Box, Color, LayoutMetadata, Margins, PageSize } from "document-content-model";
2
+ import { Alignment, AlignmentSchema, Box, Color, ContentParagraph, ContentShape, ContentSlide, ContentTable, LayoutMetadata, Margins, PageSize } from "document-content-model";
3
3
  //#region src/model/node.d.ts
4
4
  declare const AttributeSchema: z.ZodObject<{
5
5
  name: z.ZodString;
@@ -480,6 +480,7 @@ declare function ensureSpan(paragraph: XmlElement, start: number, end: number, s
480
480
  declare function rootElement(nodes: readonly XmlNode[]): XmlElement | undefined;
481
481
  declare function findChildElement(nodes: readonly XmlNode[], tag: string): XmlElement | undefined;
482
482
  declare function childrenWithTag(element: XmlElement, tag: string): XmlElement[];
483
+ declare function elementsWithTag(nodes: readonly XmlNode[], tag: string): XmlElement[];
483
484
  declare function attrValue(element: XmlElement, name: string): string | undefined;
484
485
  //#endregion
485
486
  //#region src/typed/shared/a1.d.ts
@@ -518,10 +519,56 @@ interface StyleCascadeResult {
518
519
  properties: StyleProperties;
519
520
  diagnostics: CascadeDiagnostic[];
520
521
  }
522
+ interface StyleElementChainResult {
523
+ elements: XmlElement[];
524
+ diagnostics: CascadeDiagnostic[];
525
+ }
526
+ declare function resolveStyleElementChain(styleName: string | undefined, family: StyleFamily, pkg: Package): StyleElementChainResult;
521
527
  declare function resolveStyle(styleName: string | undefined, family: StyleFamily, pkg: Package): StyleCascadeResult;
528
+ declare function findStyleElement(styleName: string, family: StyleFamily, pkg: Package): XmlElement | undefined;
522
529
  //#endregion
523
530
  //#region src/typed/shared/metadata.d.ts
524
531
  declare const META_PART = "meta.xml";
525
532
  declare function readOdfMetadata(pkg: Package): LayoutMetadata;
526
533
  //#endregion
527
- export { type Alignment, AlignmentSchema, type Attribute, AttributeSchema, type BinaryPart, BinaryPartSchema, type BuildManifestOptions, type CascadeDiagnostic, type ImageFormat, type InternRequest, type LengthUnit, MANIFEST_PART, META_PART, MIMETYPE_PART, type Manifest, type ManifestEntry, ManifestEntrySchema, type ManifestProblem, ManifestProblemSchema, ManifestSchema, ODF_MEDIA_TYPES, ODF_NAMESPACES, type OdfExtension, type OdfNamespacePrefix, type OtherPartRef, type Package, PackageSchema, type ParsedProperties, type Part, PartSchema, STYLE_FAMILIES, type StyleCascadeResult, type StyleFamily, type StyleProperties, StylePropertiesSchema, StyleRegistry, type StyleRegistryOptions, TableCursor, type XmlCdata, XmlCdataSchema, type XmlComment, XmlCommentSchema, type XmlDeclaration, XmlDeclarationSchema, type XmlElement, XmlElementSchema, type XmlNode, XmlNodeSchema, type XmlPart, XmlPartSchema, type XmlPi, XmlPiSchema, type XmlText, XmlTextSchema, type ZipEntry, attrValue, base64ToBytes, buildManifest, buildStylePropertyElements, buildXml, bytesToBase64, canonicalPropertiesString, cellReference, childrenWithTag, columnIndexToLetters, decodeOdfText, decodePackage, decodeXmlText, el, encodePackage, encodeXmlText, ensureSpan, findChildElement, formatOdfColor, formatOdfLength, formatPercentageMultiplier, formatPt, getOdfSpaceCount, isStyleFamily, isXmlNode, measureOdfNodeLength, mediaTypeForExtension, packageCodec, paragraphPropertiesToAttributes, parseBox, parseLength, parseMargins, parseOdfColor, parseOdfLength, parsePackage, parsePageSize, parseParagraphProperties, parseStyleElementProperties, parseTextProperties, parseXml, readManifest, readMimetype, readOdfMetadata, resolveStyle, rootElement, serializePackage, setDocumentMediaType, sniffImageFormat, sumOdfNodeLength, syncManifest, textPropertiesToAttributes, txt, unzipPackage, validateManifest, writeManifest, writeMimetype, xmlCodec, xmlnsAttributes, zipPackage };
534
+ //#region src/typed/shared/paragraph.d.ts
535
+ declare function readOdfParagraph(pElement: XmlElement, pkg: Package): ContentParagraph;
536
+ //#endregion
537
+ //#region src/typed/shared/table.d.ts
538
+ declare function readOdfTable(tableElement: XmlElement, pkg: Package): ContentTable;
539
+ //#endregion
540
+ //#region src/typed/shared/transform.d.ts
541
+ type OdfTransformFunction = {
542
+ readonly kind: 'rotate';
543
+ readonly angleRad: number;
544
+ } | {
545
+ readonly kind: 'translate';
546
+ readonly xPt: number;
547
+ readonly yPt: number;
548
+ };
549
+ interface OdfPoint {
550
+ readonly xPt: number;
551
+ readonly yPt: number;
552
+ }
553
+ declare function parseOdfTransform(value: string): OdfTransformFunction[];
554
+ declare function applyOdfTransform(functions: readonly OdfTransformFunction[], point: OdfPoint): OdfPoint;
555
+ declare function netRotationDeg(functions: readonly OdfTransformFunction[]): number;
556
+ interface OdfShapeGeometry {
557
+ readonly frame: Box;
558
+ readonly rotationDeg: number | undefined;
559
+ }
560
+ declare function resolveOdfShapeGeometry(element: XmlElement): OdfShapeGeometry | undefined;
561
+ declare function composeOdfGroupTransform(groupFunctions: readonly OdfTransformFunction[], child: OdfShapeGeometry): OdfShapeGeometry;
562
+ //#endregion
563
+ //#region src/typed/draw/shapes.d.ts
564
+ declare function readDrawFrame(frame: XmlElement, groupFunctions: readonly OdfTransformFunction[], pkg: Package): ContentShape | undefined;
565
+ declare function walkDrawShapes(children: readonly XmlNode[], groupFunctions: readonly OdfTransformFunction[], pkg: Package, out: ContentShape[]): void;
566
+ //#endregion
567
+ //#region src/typed/odp/read.d.ts
568
+ interface OdpDocument {
569
+ metadata: LayoutMetadata;
570
+ slides: ContentSlide[];
571
+ }
572
+ declare function readOdp(pkg: Package): OdpDocument;
573
+ //#endregion
574
+ export { type Alignment, AlignmentSchema, type Attribute, AttributeSchema, type BinaryPart, BinaryPartSchema, type BuildManifestOptions, type CascadeDiagnostic, type ImageFormat, type InternRequest, type LengthUnit, MANIFEST_PART, META_PART, MIMETYPE_PART, type Manifest, type ManifestEntry, ManifestEntrySchema, type ManifestProblem, ManifestProblemSchema, ManifestSchema, ODF_MEDIA_TYPES, ODF_NAMESPACES, type OdfExtension, type OdfNamespacePrefix, type OdfPoint, type OdfShapeGeometry, type OdfTransformFunction, type OdpDocument, type OtherPartRef, type Package, PackageSchema, type ParsedProperties, type Part, PartSchema, STYLE_FAMILIES, type StyleCascadeResult, type StyleElementChainResult, type StyleFamily, type StyleProperties, StylePropertiesSchema, StyleRegistry, type StyleRegistryOptions, TableCursor, type XmlCdata, XmlCdataSchema, type XmlComment, XmlCommentSchema, type XmlDeclaration, XmlDeclarationSchema, type XmlElement, XmlElementSchema, type XmlNode, XmlNodeSchema, type XmlPart, XmlPartSchema, type XmlPi, XmlPiSchema, type XmlText, XmlTextSchema, type ZipEntry, applyOdfTransform, attrValue, base64ToBytes, buildManifest, buildStylePropertyElements, buildXml, bytesToBase64, canonicalPropertiesString, cellReference, childrenWithTag, columnIndexToLetters, composeOdfGroupTransform, decodeOdfText, decodePackage, decodeXmlText, el, elementsWithTag, encodePackage, encodeXmlText, ensureSpan, findChildElement, findStyleElement, formatOdfColor, formatOdfLength, formatPercentageMultiplier, formatPt, getOdfSpaceCount, isStyleFamily, isXmlNode, measureOdfNodeLength, mediaTypeForExtension, netRotationDeg, packageCodec, paragraphPropertiesToAttributes, parseBox, parseLength, parseMargins, parseOdfColor, parseOdfLength, parseOdfTransform, parsePackage, parsePageSize, parseParagraphProperties, parseStyleElementProperties, parseTextProperties, parseXml, readDrawFrame, readManifest, readMimetype, readOdfMetadata, readOdfParagraph, readOdfTable, readOdp, resolveOdfShapeGeometry, resolveStyle, resolveStyleElementChain, rootElement, serializePackage, setDocumentMediaType, sniffImageFormat, sumOdfNodeLength, syncManifest, textPropertiesToAttributes, txt, unzipPackage, validateManifest, walkDrawShapes, writeManifest, writeMimetype, xmlCodec, xmlnsAttributes, zipPackage };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { Alignment, AlignmentSchema, Box, Color, LayoutMetadata, Margins, PageSize } from "document-content-model";
2
+ import { Alignment, AlignmentSchema, Box, Color, ContentParagraph, ContentShape, ContentSlide, ContentTable, LayoutMetadata, Margins, PageSize } from "document-content-model";
3
3
  //#region src/model/node.d.ts
4
4
  declare const AttributeSchema: z.ZodObject<{
5
5
  name: z.ZodString;
@@ -480,6 +480,7 @@ declare function ensureSpan(paragraph: XmlElement, start: number, end: number, s
480
480
  declare function rootElement(nodes: readonly XmlNode[]): XmlElement | undefined;
481
481
  declare function findChildElement(nodes: readonly XmlNode[], tag: string): XmlElement | undefined;
482
482
  declare function childrenWithTag(element: XmlElement, tag: string): XmlElement[];
483
+ declare function elementsWithTag(nodes: readonly XmlNode[], tag: string): XmlElement[];
483
484
  declare function attrValue(element: XmlElement, name: string): string | undefined;
484
485
  //#endregion
485
486
  //#region src/typed/shared/a1.d.ts
@@ -518,10 +519,56 @@ interface StyleCascadeResult {
518
519
  properties: StyleProperties;
519
520
  diagnostics: CascadeDiagnostic[];
520
521
  }
522
+ interface StyleElementChainResult {
523
+ elements: XmlElement[];
524
+ diagnostics: CascadeDiagnostic[];
525
+ }
526
+ declare function resolveStyleElementChain(styleName: string | undefined, family: StyleFamily, pkg: Package): StyleElementChainResult;
521
527
  declare function resolveStyle(styleName: string | undefined, family: StyleFamily, pkg: Package): StyleCascadeResult;
528
+ declare function findStyleElement(styleName: string, family: StyleFamily, pkg: Package): XmlElement | undefined;
522
529
  //#endregion
523
530
  //#region src/typed/shared/metadata.d.ts
524
531
  declare const META_PART = "meta.xml";
525
532
  declare function readOdfMetadata(pkg: Package): LayoutMetadata;
526
533
  //#endregion
527
- export { type Alignment, AlignmentSchema, type Attribute, AttributeSchema, type BinaryPart, BinaryPartSchema, type BuildManifestOptions, type CascadeDiagnostic, type ImageFormat, type InternRequest, type LengthUnit, MANIFEST_PART, META_PART, MIMETYPE_PART, type Manifest, type ManifestEntry, ManifestEntrySchema, type ManifestProblem, ManifestProblemSchema, ManifestSchema, ODF_MEDIA_TYPES, ODF_NAMESPACES, type OdfExtension, type OdfNamespacePrefix, type OtherPartRef, type Package, PackageSchema, type ParsedProperties, type Part, PartSchema, STYLE_FAMILIES, type StyleCascadeResult, type StyleFamily, type StyleProperties, StylePropertiesSchema, StyleRegistry, type StyleRegistryOptions, TableCursor, type XmlCdata, XmlCdataSchema, type XmlComment, XmlCommentSchema, type XmlDeclaration, XmlDeclarationSchema, type XmlElement, XmlElementSchema, type XmlNode, XmlNodeSchema, type XmlPart, XmlPartSchema, type XmlPi, XmlPiSchema, type XmlText, XmlTextSchema, type ZipEntry, attrValue, base64ToBytes, buildManifest, buildStylePropertyElements, buildXml, bytesToBase64, canonicalPropertiesString, cellReference, childrenWithTag, columnIndexToLetters, decodeOdfText, decodePackage, decodeXmlText, el, encodePackage, encodeXmlText, ensureSpan, findChildElement, formatOdfColor, formatOdfLength, formatPercentageMultiplier, formatPt, getOdfSpaceCount, isStyleFamily, isXmlNode, measureOdfNodeLength, mediaTypeForExtension, packageCodec, paragraphPropertiesToAttributes, parseBox, parseLength, parseMargins, parseOdfColor, parseOdfLength, parsePackage, parsePageSize, parseParagraphProperties, parseStyleElementProperties, parseTextProperties, parseXml, readManifest, readMimetype, readOdfMetadata, resolveStyle, rootElement, serializePackage, setDocumentMediaType, sniffImageFormat, sumOdfNodeLength, syncManifest, textPropertiesToAttributes, txt, unzipPackage, validateManifest, writeManifest, writeMimetype, xmlCodec, xmlnsAttributes, zipPackage };
534
+ //#region src/typed/shared/paragraph.d.ts
535
+ declare function readOdfParagraph(pElement: XmlElement, pkg: Package): ContentParagraph;
536
+ //#endregion
537
+ //#region src/typed/shared/table.d.ts
538
+ declare function readOdfTable(tableElement: XmlElement, pkg: Package): ContentTable;
539
+ //#endregion
540
+ //#region src/typed/shared/transform.d.ts
541
+ type OdfTransformFunction = {
542
+ readonly kind: 'rotate';
543
+ readonly angleRad: number;
544
+ } | {
545
+ readonly kind: 'translate';
546
+ readonly xPt: number;
547
+ readonly yPt: number;
548
+ };
549
+ interface OdfPoint {
550
+ readonly xPt: number;
551
+ readonly yPt: number;
552
+ }
553
+ declare function parseOdfTransform(value: string): OdfTransformFunction[];
554
+ declare function applyOdfTransform(functions: readonly OdfTransformFunction[], point: OdfPoint): OdfPoint;
555
+ declare function netRotationDeg(functions: readonly OdfTransformFunction[]): number;
556
+ interface OdfShapeGeometry {
557
+ readonly frame: Box;
558
+ readonly rotationDeg: number | undefined;
559
+ }
560
+ declare function resolveOdfShapeGeometry(element: XmlElement): OdfShapeGeometry | undefined;
561
+ declare function composeOdfGroupTransform(groupFunctions: readonly OdfTransformFunction[], child: OdfShapeGeometry): OdfShapeGeometry;
562
+ //#endregion
563
+ //#region src/typed/draw/shapes.d.ts
564
+ declare function readDrawFrame(frame: XmlElement, groupFunctions: readonly OdfTransformFunction[], pkg: Package): ContentShape | undefined;
565
+ declare function walkDrawShapes(children: readonly XmlNode[], groupFunctions: readonly OdfTransformFunction[], pkg: Package, out: ContentShape[]): void;
566
+ //#endregion
567
+ //#region src/typed/odp/read.d.ts
568
+ interface OdpDocument {
569
+ metadata: LayoutMetadata;
570
+ slides: ContentSlide[];
571
+ }
572
+ declare function readOdp(pkg: Package): OdpDocument;
573
+ //#endregion
574
+ export { type Alignment, AlignmentSchema, type Attribute, AttributeSchema, type BinaryPart, BinaryPartSchema, type BuildManifestOptions, type CascadeDiagnostic, type ImageFormat, type InternRequest, type LengthUnit, MANIFEST_PART, META_PART, MIMETYPE_PART, type Manifest, type ManifestEntry, ManifestEntrySchema, type ManifestProblem, ManifestProblemSchema, ManifestSchema, ODF_MEDIA_TYPES, ODF_NAMESPACES, type OdfExtension, type OdfNamespacePrefix, type OdfPoint, type OdfShapeGeometry, type OdfTransformFunction, type OdpDocument, type OtherPartRef, type Package, PackageSchema, type ParsedProperties, type Part, PartSchema, STYLE_FAMILIES, type StyleCascadeResult, type StyleElementChainResult, type StyleFamily, type StyleProperties, StylePropertiesSchema, StyleRegistry, type StyleRegistryOptions, TableCursor, type XmlCdata, XmlCdataSchema, type XmlComment, XmlCommentSchema, type XmlDeclaration, XmlDeclarationSchema, type XmlElement, XmlElementSchema, type XmlNode, XmlNodeSchema, type XmlPart, XmlPartSchema, type XmlPi, XmlPiSchema, type XmlText, XmlTextSchema, type ZipEntry, applyOdfTransform, attrValue, base64ToBytes, buildManifest, buildStylePropertyElements, buildXml, bytesToBase64, canonicalPropertiesString, cellReference, childrenWithTag, columnIndexToLetters, composeOdfGroupTransform, decodeOdfText, decodePackage, decodeXmlText, el, elementsWithTag, encodePackage, encodeXmlText, ensureSpan, findChildElement, findStyleElement, formatOdfColor, formatOdfLength, formatPercentageMultiplier, formatPt, getOdfSpaceCount, isStyleFamily, isXmlNode, measureOdfNodeLength, mediaTypeForExtension, netRotationDeg, packageCodec, paragraphPropertiesToAttributes, parseBox, parseLength, parseMargins, parseOdfColor, parseOdfLength, parseOdfTransform, parsePackage, parsePageSize, parseParagraphProperties, parseStyleElementProperties, parseTextProperties, parseXml, readDrawFrame, readManifest, readMimetype, readOdfMetadata, readOdfParagraph, readOdfTable, readOdp, resolveOdfShapeGeometry, resolveStyle, resolveStyleElementChain, rootElement, serializePackage, setDocumentMediaType, sniffImageFormat, sumOdfNodeLength, syncManifest, textPropertiesToAttributes, txt, unzipPackage, validateManifest, walkDrawShapes, writeManifest, writeMimetype, xmlCodec, xmlnsAttributes, zipPackage };
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { XMLBuilder, XMLParser } from "fast-xml-parser";
3
3
  import { unzipSync, zipSync } from "fflate";
4
- import { AlignmentSchema, AlignmentSchema as AlignmentSchema$1, ColorSchema, colorToRgbHex, rgbHexToColor } from "document-content-model";
4
+ import { AlignmentSchema, AlignmentSchema as AlignmentSchema$1, ColorSchema, SLIDE_SIZE_WIDESCREEN, colorToRgbHex, rgbHexToColor } from "document-content-model";
5
5
  //#region src/model/node.ts
6
6
  const AttributeSchema = z.object({
7
7
  name: z.string(),
@@ -1242,6 +1242,17 @@ function childrenWithTag(element, tag) {
1242
1242
  for (const child of element.children) if (child.type === "element" && child.tag === tag) out.push(child);
1243
1243
  return out;
1244
1244
  }
1245
+ function* walk(nodes) {
1246
+ for (const node of nodes) {
1247
+ yield node;
1248
+ if (node.type === "element") yield* walk(node.children);
1249
+ }
1250
+ }
1251
+ function elementsWithTag(nodes, tag) {
1252
+ const out = [];
1253
+ for (const node of walk(nodes)) if (node.type === "element" && node.tag === tag) out.push(node);
1254
+ return out;
1255
+ }
1245
1256
  function attrValue(element, name) {
1246
1257
  return element.attributes.find((attribute) => attribute.name === name)?.value;
1247
1258
  }
@@ -1521,13 +1532,14 @@ function collectStyles(pkg) {
1521
1532
  defaultByFamily
1522
1533
  };
1523
1534
  }
1524
- function resolveStyle(styleName, family, pkg) {
1535
+ function resolveStyleElementChain(styleName, family, pkg) {
1525
1536
  const { byName, defaultByFamily } = collectStyles(pkg);
1526
1537
  const diagnostics = [];
1538
+ const elements = [];
1527
1539
  const defaultElement = defaultByFamily.get(family);
1528
- let properties = defaultElement === void 0 ? {} : parseStyleElementProperties(defaultElement).properties;
1540
+ if (defaultElement !== void 0) elements.push(defaultElement);
1529
1541
  if (styleName === void 0) return {
1530
- properties,
1542
+ elements,
1531
1543
  diagnostics
1532
1544
  };
1533
1545
  const chain = [];
@@ -1555,7 +1567,16 @@ function resolveStyle(styleName, family, pkg) {
1555
1567
  currentName = attrValue(element, "style:parent-style-name");
1556
1568
  }
1557
1569
  chain.reverse();
1558
- for (const element of chain) properties = {
1570
+ elements.push(...chain);
1571
+ return {
1572
+ elements,
1573
+ diagnostics
1574
+ };
1575
+ }
1576
+ function resolveStyle(styleName, family, pkg) {
1577
+ const { elements, diagnostics } = resolveStyleElementChain(styleName, family, pkg);
1578
+ let properties = {};
1579
+ for (const element of elements) properties = {
1559
1580
  ...properties,
1560
1581
  ...parseStyleElementProperties(element).properties
1561
1582
  };
@@ -1564,6 +1585,10 @@ function resolveStyle(styleName, family, pkg) {
1564
1585
  diagnostics
1565
1586
  };
1566
1587
  }
1588
+ function findStyleElement(styleName, family, pkg) {
1589
+ const { byName } = collectStyles(pkg);
1590
+ return byName.get(nameKey(family, styleName));
1591
+ }
1567
1592
  //#endregion
1568
1593
  //#region src/typed/shared/metadata.ts
1569
1594
  const META_PART = "meta.xml";
@@ -1602,4 +1627,374 @@ function readOdfMetadata(pkg) {
1602
1627
  return metadata;
1603
1628
  }
1604
1629
  //#endregion
1605
- export { AlignmentSchema, AttributeSchema, BinaryPartSchema, MANIFEST_PART, META_PART, MIMETYPE_PART, ManifestEntrySchema, ManifestProblemSchema, ManifestSchema, ODF_MEDIA_TYPES, ODF_NAMESPACES, PackageSchema, PartSchema, STYLE_FAMILIES, StylePropertiesSchema, StyleRegistry, TableCursor, XmlCdataSchema, XmlCommentSchema, XmlDeclarationSchema, XmlElementSchema, XmlNodeSchema, XmlPartSchema, XmlPiSchema, XmlTextSchema, attrValue, base64ToBytes, buildManifest, buildStylePropertyElements, buildXml, bytesToBase64, canonicalPropertiesString, cellReference, childrenWithTag, columnIndexToLetters, decodeOdfText, decodePackage, decodeXmlText, el, encodePackage, encodeXmlText, ensureSpan, findChildElement, formatOdfColor, formatOdfLength, formatPercentageMultiplier, formatPt, getOdfSpaceCount, isStyleFamily, isXmlNode, measureOdfNodeLength, mediaTypeForExtension, packageCodec, paragraphPropertiesToAttributes, parseBox, parseLength, parseMargins, parseOdfColor, parseOdfLength, parsePackage, parsePageSize, parseParagraphProperties, parseStyleElementProperties, parseTextProperties, parseXml, readManifest, readMimetype, readOdfMetadata, resolveStyle, rootElement, serializePackage, setDocumentMediaType, sniffImageFormat, sumOdfNodeLength, syncManifest, textPropertiesToAttributes, txt, unzipPackage, validateManifest, writeManifest, writeMimetype, xmlCodec, xmlnsAttributes, zipPackage };
1630
+ //#region src/typed/shared/paragraph.ts
1631
+ function collectRuns(nodes, baseProperties, pkg, out) {
1632
+ for (const node of nodes) {
1633
+ if (node.type === "text") {
1634
+ if (node.value.length > 0) out.push(runFromText(decodeXmlText(node.value), baseProperties));
1635
+ continue;
1636
+ }
1637
+ if (node.type !== "element") continue;
1638
+ if (node.tag === "text:s") out.push(runFromText(" ".repeat(getOdfSpaceCount(node)), baseProperties));
1639
+ else if (node.tag === "text:tab") out.push(runFromText(" ", baseProperties));
1640
+ else if (node.tag === "text:line-break") out.push(runFromText("\n", baseProperties));
1641
+ else if (node.tag === "text:span") {
1642
+ const styleName = attrValue(node, "text:style-name");
1643
+ const spanProperties = {
1644
+ ...baseProperties,
1645
+ ...resolveStyle(styleName, "text", pkg).properties
1646
+ };
1647
+ collectRuns(node.children, spanProperties, pkg, out);
1648
+ }
1649
+ }
1650
+ }
1651
+ function runFromText(text, properties) {
1652
+ return {
1653
+ text,
1654
+ bold: properties.bold,
1655
+ italic: properties.italic,
1656
+ underline: properties.underline,
1657
+ strike: properties.strike,
1658
+ fontFamily: properties.fontFamily,
1659
+ sizePt: properties.sizePt,
1660
+ color: properties.color
1661
+ };
1662
+ }
1663
+ function readOdfParagraph(pElement, pkg) {
1664
+ const styleName = attrValue(pElement, "text:style-name");
1665
+ const paragraphProperties = resolveStyle(styleName, "paragraph", pkg).properties;
1666
+ const runs = [];
1667
+ collectRuns(pElement.children, paragraphProperties, pkg, runs);
1668
+ return {
1669
+ kind: "paragraph",
1670
+ runs,
1671
+ styleId: styleName,
1672
+ alignment: paragraphProperties.alignment,
1673
+ spacingBeforePt: paragraphProperties.spacingBeforePt,
1674
+ spacingAfterPt: paragraphProperties.spacingAfterPt,
1675
+ lineSpacing: paragraphProperties.lineSpacing,
1676
+ indentLeftPt: paragraphProperties.indentLeftPt,
1677
+ indentFirstLinePt: paragraphProperties.indentFirstLinePt
1678
+ };
1679
+ }
1680
+ //#endregion
1681
+ //#region src/typed/shared/table.ts
1682
+ function readRepeatCount(element, attrName) {
1683
+ const raw = attrValue(element, attrName);
1684
+ if (raw === void 0) return 1;
1685
+ const parsed = Number.parseInt(raw, 10);
1686
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : 1;
1687
+ }
1688
+ function resolveColumnWidthPt(columnElement, pkg) {
1689
+ const styleName = attrValue(columnElement, "table:style-name");
1690
+ const styleElement = styleName === void 0 ? void 0 : findStyleElement(styleName, "table-column", pkg);
1691
+ const props = styleElement === void 0 ? void 0 : childrenWithTag(styleElement, "style:table-column-properties")[0];
1692
+ const widthValue = props === void 0 ? void 0 : attrValue(props, "style:column-width");
1693
+ return widthValue === void 0 ? 0 : parseOdfLength(widthValue) ?? 0;
1694
+ }
1695
+ function resolveRowHeightPt(rowElement, pkg) {
1696
+ const styleName = attrValue(rowElement, "table:style-name");
1697
+ const styleElement = styleName === void 0 ? void 0 : findStyleElement(styleName, "table-row", pkg);
1698
+ const props = styleElement === void 0 ? void 0 : childrenWithTag(styleElement, "style:table-row-properties")[0];
1699
+ const heightValue = props === void 0 ? void 0 : attrValue(props, "style:row-height");
1700
+ return heightValue === void 0 ? void 0 : parseOdfLength(heightValue);
1701
+ }
1702
+ function readTableCellBackground(cellElement, pkg) {
1703
+ const styleName = attrValue(cellElement, "table:style-name");
1704
+ const styleElement = styleName === void 0 ? void 0 : findStyleElement(styleName, "table-cell", pkg);
1705
+ const props = styleElement === void 0 ? void 0 : childrenWithTag(styleElement, "style:table-cell-properties")[0];
1706
+ const value = props === void 0 ? void 0 : attrValue(props, "fo:background-color");
1707
+ return value === void 0 ? void 0 : parseOdfColor(value);
1708
+ }
1709
+ function readTableCell(cellElement, pkg) {
1710
+ const blocks = childrenWithTag(cellElement, "text:p").map((p) => readOdfParagraph(p, pkg));
1711
+ const colSpanRaw = attrValue(cellElement, "table:number-columns-spanned");
1712
+ const rowSpanRaw = attrValue(cellElement, "table:number-rows-spanned");
1713
+ return {
1714
+ blocks,
1715
+ colSpan: colSpanRaw === void 0 ? void 0 : Number.parseInt(colSpanRaw, 10),
1716
+ rowSpan: rowSpanRaw === void 0 ? void 0 : Number.parseInt(rowSpanRaw, 10),
1717
+ background: readTableCellBackground(cellElement, pkg)
1718
+ };
1719
+ }
1720
+ function readTableRow(rowElement, pkg) {
1721
+ const cells = [];
1722
+ for (const child of rowElement.children) {
1723
+ if (child.type !== "element") continue;
1724
+ if (child.tag === "table:covered-table-cell") {
1725
+ const repeat = readRepeatCount(child, "table:number-columns-repeated");
1726
+ for (let i = 0; i < repeat; i++) cells.push({ blocks: [] });
1727
+ } else if (child.tag === "table:table-cell") {
1728
+ const cell = readTableCell(child, pkg);
1729
+ const repeat = readRepeatCount(child, "table:number-columns-repeated");
1730
+ for (let i = 0; i < repeat; i++) cells.push(cell);
1731
+ }
1732
+ }
1733
+ return {
1734
+ cells,
1735
+ heightPt: resolveRowHeightPt(rowElement, pkg)
1736
+ };
1737
+ }
1738
+ function readOdfTable(tableElement, pkg) {
1739
+ const columnWidthsPt = [];
1740
+ for (const column of childrenWithTag(tableElement, "table:table-column")) {
1741
+ const widthPt = resolveColumnWidthPt(column, pkg);
1742
+ const repeat = readRepeatCount(column, "table:number-columns-repeated");
1743
+ for (let i = 0; i < repeat; i++) columnWidthsPt.push(widthPt);
1744
+ }
1745
+ const rows = [];
1746
+ for (const rowElement of childrenWithTag(tableElement, "table:table-row")) {
1747
+ const row = readTableRow(rowElement, pkg);
1748
+ const repeat = readRepeatCount(rowElement, "table:number-rows-repeated");
1749
+ for (let i = 0; i < repeat; i++) rows.push(row);
1750
+ }
1751
+ return {
1752
+ kind: "table",
1753
+ rows,
1754
+ columnWidthsPt
1755
+ };
1756
+ }
1757
+ //#endregion
1758
+ //#region src/typed/shared/transform.ts
1759
+ const FUNCTION_PATTERN = /([a-zA-Z]+)\s*\(\s*([^)]*?)\s*\)/g;
1760
+ function parseOdfTransform(value) {
1761
+ const functions = [];
1762
+ for (const match of value.matchAll(FUNCTION_PATTERN)) {
1763
+ const name = match[1];
1764
+ const argsRaw = match[2];
1765
+ if (name === void 0 || argsRaw === void 0) continue;
1766
+ const args = argsRaw.split(/\s+/).filter((arg) => arg.length > 0);
1767
+ if (name === "rotate") {
1768
+ const angleArg = args[0];
1769
+ if (angleArg === void 0) continue;
1770
+ const angleRad = Number(angleArg);
1771
+ if (!Number.isFinite(angleRad)) continue;
1772
+ functions.push({
1773
+ kind: "rotate",
1774
+ angleRad
1775
+ });
1776
+ } else if (name === "translate") {
1777
+ const xArg = args[0];
1778
+ if (xArg === void 0) continue;
1779
+ const yArg = args[1];
1780
+ const xPt = parseOdfLength(xArg);
1781
+ const yPt = yArg === void 0 ? 0 : parseOdfLength(yArg);
1782
+ if (xPt === void 0 || yPt === void 0) continue;
1783
+ functions.push({
1784
+ kind: "translate",
1785
+ xPt,
1786
+ yPt
1787
+ });
1788
+ }
1789
+ }
1790
+ return functions;
1791
+ }
1792
+ function applyOdfTransform(functions, point) {
1793
+ let current = point;
1794
+ for (const fn of functions) if (fn.kind === "rotate") {
1795
+ const cos = Math.cos(fn.angleRad);
1796
+ const sin = Math.sin(fn.angleRad);
1797
+ current = {
1798
+ xPt: current.xPt * cos + current.yPt * sin,
1799
+ yPt: current.yPt * cos - current.xPt * sin
1800
+ };
1801
+ } else current = {
1802
+ xPt: current.xPt + fn.xPt,
1803
+ yPt: current.yPt + fn.yPt
1804
+ };
1805
+ return current;
1806
+ }
1807
+ function netRotationDeg(functions) {
1808
+ let totalRad = 0;
1809
+ for (const fn of functions) if (fn.kind === "rotate") totalRad += fn.angleRad;
1810
+ return -totalRad * 180 / Math.PI;
1811
+ }
1812
+ function resolveOdfShapeGeometry(element) {
1813
+ const transformValue = attrValue(element, "draw:transform");
1814
+ if (transformValue === void 0) {
1815
+ const box = parseBox(element);
1816
+ return box === void 0 ? void 0 : {
1817
+ frame: box,
1818
+ rotationDeg: void 0
1819
+ };
1820
+ }
1821
+ const widthValue = attrValue(element, "svg:width");
1822
+ const heightValue = attrValue(element, "svg:height");
1823
+ if (widthValue === void 0 || heightValue === void 0) return;
1824
+ const widthPt = parseOdfLength(widthValue);
1825
+ const heightPt = parseOdfLength(heightValue);
1826
+ if (widthPt === void 0 || heightPt === void 0) return;
1827
+ const functions = parseOdfTransform(transformValue);
1828
+ const center = applyOdfTransform(functions, {
1829
+ xPt: widthPt / 2,
1830
+ yPt: heightPt / 2
1831
+ });
1832
+ const rotationDeg = netRotationDeg(functions);
1833
+ return {
1834
+ frame: {
1835
+ xPt: center.xPt - widthPt / 2,
1836
+ yPt: center.yPt - heightPt / 2,
1837
+ widthPt,
1838
+ heightPt
1839
+ },
1840
+ rotationDeg: rotationDeg === 0 ? void 0 : rotationDeg
1841
+ };
1842
+ }
1843
+ function composeOdfGroupTransform(groupFunctions, child) {
1844
+ if (groupFunctions.length === 0) return child;
1845
+ const newCenter = applyOdfTransform(groupFunctions, {
1846
+ xPt: child.frame.xPt + child.frame.widthPt / 2,
1847
+ yPt: child.frame.yPt + child.frame.heightPt / 2
1848
+ });
1849
+ const newRotationDeg = (child.rotationDeg ?? 0) + netRotationDeg(groupFunctions);
1850
+ return {
1851
+ frame: {
1852
+ xPt: newCenter.xPt - child.frame.widthPt / 2,
1853
+ yPt: newCenter.yPt - child.frame.heightPt / 2,
1854
+ widthPt: child.frame.widthPt,
1855
+ heightPt: child.frame.heightPt
1856
+ },
1857
+ rotationDeg: newRotationDeg === 0 ? void 0 : newRotationDeg
1858
+ };
1859
+ }
1860
+ //#endregion
1861
+ //#region src/typed/draw/shapes.ts
1862
+ const ZERO_INSETS = {
1863
+ insetLeftPt: 0,
1864
+ insetTopPt: 0,
1865
+ insetRightPt: 0,
1866
+ insetBottomPt: 0
1867
+ };
1868
+ function readPaddingPt(props, attrName) {
1869
+ const value = attrValue(props, attrName);
1870
+ return value === void 0 ? void 0 : parseOdfLength(value);
1871
+ }
1872
+ function readFrameInsets(frame, pkg) {
1873
+ const { elements } = resolveStyleElementChain(attrValue(frame, "draw:style-name"), "graphic", pkg);
1874
+ let insets = ZERO_INSETS;
1875
+ for (const element of elements) {
1876
+ const props = childrenWithTag(element, "style:graphic-properties")[0];
1877
+ if (props === void 0) continue;
1878
+ insets = {
1879
+ insetLeftPt: readPaddingPt(props, "fo:padding-left") ?? insets.insetLeftPt,
1880
+ insetTopPt: readPaddingPt(props, "fo:padding-top") ?? insets.insetTopPt,
1881
+ insetRightPt: readPaddingPt(props, "fo:padding-right") ?? insets.insetRightPt,
1882
+ insetBottomPt: readPaddingPt(props, "fo:padding-bottom") ?? insets.insetBottomPt
1883
+ };
1884
+ }
1885
+ return insets;
1886
+ }
1887
+ function readDrawImageBlock(image, frameBox, pkg) {
1888
+ const href = attrValue(image, "xlink:href");
1889
+ const part = href === void 0 ? void 0 : pkg.parts[href];
1890
+ if (part?.kind !== "binary") return;
1891
+ const format = sniffImageFormat(base64ToBytes(part.base64));
1892
+ if (format === void 0) return;
1893
+ return {
1894
+ kind: "image",
1895
+ format,
1896
+ base64: part.base64,
1897
+ widthPt: frameBox.widthPt,
1898
+ heightPt: frameBox.heightPt
1899
+ };
1900
+ }
1901
+ function readDrawFrameContent(frame, frameBox, pkg) {
1902
+ const table = childrenWithTag(frame, "table:table")[0];
1903
+ if (table !== void 0) return [readOdfTable(table, pkg)];
1904
+ const textBox = childrenWithTag(frame, "draw:text-box")[0];
1905
+ if (textBox !== void 0) return elementsWithTag(textBox.children, "text:p").map((p) => readOdfParagraph(p, pkg));
1906
+ const image = childrenWithTag(frame, "draw:image")[0];
1907
+ if (image !== void 0) {
1908
+ const block = readDrawImageBlock(image, frameBox, pkg);
1909
+ return block === void 0 ? [] : [block];
1910
+ }
1911
+ return [];
1912
+ }
1913
+ function readDrawFrame(frame, groupFunctions, pkg) {
1914
+ const ownGeometry = resolveOdfShapeGeometry(frame);
1915
+ if (ownGeometry === void 0) return;
1916
+ const geometry = composeOdfGroupTransform(groupFunctions, ownGeometry);
1917
+ return {
1918
+ name: attrValue(frame, "draw:name"),
1919
+ frame: geometry.frame,
1920
+ rotationDeg: geometry.rotationDeg,
1921
+ ...readFrameInsets(frame, pkg),
1922
+ blocks: readDrawFrameContent(frame, geometry.frame, pkg)
1923
+ };
1924
+ }
1925
+ function readOwnTransformFunctions(element) {
1926
+ const value = attrValue(element, "draw:transform");
1927
+ return value === void 0 ? [] : parseOdfTransform(value);
1928
+ }
1929
+ function walkDrawShapes(children, groupFunctions, pkg, out) {
1930
+ for (const node of children) {
1931
+ if (node.type !== "element") continue;
1932
+ if (node.tag === "draw:frame") {
1933
+ const shape = readDrawFrame(node, groupFunctions, pkg);
1934
+ if (shape !== void 0) out.push(shape);
1935
+ } else if (node.tag === "draw:g") {
1936
+ const ownFunctions = readOwnTransformFunctions(node);
1937
+ const nested = ownFunctions.length === 0 ? groupFunctions : [...ownFunctions, ...groupFunctions];
1938
+ walkDrawShapes(node.children, nested, pkg, out);
1939
+ }
1940
+ }
1941
+ }
1942
+ //#endregion
1943
+ //#region src/typed/odp/read.ts
1944
+ const CONTENT_PART = "content.xml";
1945
+ const STYLES_PART = "styles.xml";
1946
+ const AUTOMATIC_STYLE_PARTS = [CONTENT_PART, STYLES_PART];
1947
+ function findMasterPageElement(pkg, masterPageName) {
1948
+ if (masterPageName === void 0) return;
1949
+ const stylesPart = pkg.parts[STYLES_PART];
1950
+ if (stylesPart?.kind !== "xml") return;
1951
+ const root = rootElement(stylesPart.nodes);
1952
+ const masterStyles = root === void 0 ? void 0 : findChildElement(root.children, "office:master-styles");
1953
+ if (masterStyles === void 0) return;
1954
+ return childrenWithTag(masterStyles, "style:master-page").find((element) => attrValue(element, "style:name") === masterPageName);
1955
+ }
1956
+ function findPageLayoutElement(pkg, pageLayoutName) {
1957
+ if (pageLayoutName === void 0) return;
1958
+ for (const partPath of AUTOMATIC_STYLE_PARTS) {
1959
+ const part = pkg.parts[partPath];
1960
+ if (part?.kind !== "xml") continue;
1961
+ const root = rootElement(part.nodes);
1962
+ const automaticStyles = root === void 0 ? void 0 : findChildElement(root.children, "office:automatic-styles");
1963
+ if (automaticStyles === void 0) continue;
1964
+ const found = childrenWithTag(automaticStyles, "style:page-layout").find((element) => attrValue(element, "style:name") === pageLayoutName);
1965
+ if (found !== void 0) return found;
1966
+ }
1967
+ }
1968
+ function readSlideSize(page, pkg) {
1969
+ const masterPage = findMasterPageElement(pkg, attrValue(page, "draw:master-page-name"));
1970
+ const pageLayout = findPageLayoutElement(pkg, masterPage === void 0 ? void 0 : attrValue(masterPage, "style:page-layout-name"));
1971
+ const properties = pageLayout === void 0 ? void 0 : childrenWithTag(pageLayout, "style:page-layout-properties")[0];
1972
+ return (properties === void 0 ? void 0 : parsePageSize(properties)) ?? SLIDE_SIZE_WIDESCREEN;
1973
+ }
1974
+ function readSlideNotes(page) {
1975
+ const notes = childrenWithTag(page, "presentation:notes")[0];
1976
+ if (notes === void 0) return "";
1977
+ return elementsWithTag(notes.children, "text:p").map(decodeOdfText).join("\n");
1978
+ }
1979
+ function readSlide(page, pkg) {
1980
+ const shapes = [];
1981
+ walkDrawShapes(page.children, [], pkg, shapes);
1982
+ return {
1983
+ size: readSlideSize(page, pkg),
1984
+ shapes,
1985
+ notes: readSlideNotes(page)
1986
+ };
1987
+ }
1988
+ function readOdp(pkg) {
1989
+ const contentPart = pkg.parts[CONTENT_PART];
1990
+ const root = contentPart?.kind === "xml" ? rootElement(contentPart.nodes) : void 0;
1991
+ const body = root === void 0 ? void 0 : findChildElement(root.children, "office:body");
1992
+ const presentation = body === void 0 ? void 0 : findChildElement(body.children, "office:presentation");
1993
+ const pages = presentation === void 0 ? [] : childrenWithTag(presentation, "draw:page");
1994
+ return {
1995
+ metadata: readOdfMetadata(pkg),
1996
+ slides: pages.map((page) => readSlide(page, pkg))
1997
+ };
1998
+ }
1999
+ //#endregion
2000
+ export { AlignmentSchema, AttributeSchema, BinaryPartSchema, MANIFEST_PART, META_PART, MIMETYPE_PART, ManifestEntrySchema, ManifestProblemSchema, ManifestSchema, ODF_MEDIA_TYPES, ODF_NAMESPACES, PackageSchema, PartSchema, STYLE_FAMILIES, StylePropertiesSchema, StyleRegistry, TableCursor, XmlCdataSchema, XmlCommentSchema, XmlDeclarationSchema, XmlElementSchema, XmlNodeSchema, XmlPartSchema, XmlPiSchema, XmlTextSchema, applyOdfTransform, attrValue, base64ToBytes, buildManifest, buildStylePropertyElements, buildXml, bytesToBase64, canonicalPropertiesString, cellReference, childrenWithTag, columnIndexToLetters, composeOdfGroupTransform, decodeOdfText, decodePackage, decodeXmlText, el, elementsWithTag, encodePackage, encodeXmlText, ensureSpan, findChildElement, findStyleElement, formatOdfColor, formatOdfLength, formatPercentageMultiplier, formatPt, getOdfSpaceCount, isStyleFamily, isXmlNode, measureOdfNodeLength, mediaTypeForExtension, netRotationDeg, packageCodec, paragraphPropertiesToAttributes, parseBox, parseLength, parseMargins, parseOdfColor, parseOdfLength, parseOdfTransform, parsePackage, parsePageSize, parseParagraphProperties, parseStyleElementProperties, parseTextProperties, parseXml, readDrawFrame, readManifest, readMimetype, readOdfMetadata, readOdfParagraph, readOdfTable, readOdp, resolveOdfShapeGeometry, resolveStyle, resolveStyleElementChain, rootElement, serializePackage, setDocumentMediaType, sniffImageFormat, sumOdfNodeLength, syncManifest, textPropertiesToAttributes, txt, unzipPackage, validateManifest, walkDrawShapes, writeManifest, writeMimetype, xmlCodec, xmlnsAttributes, zipPackage };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "odf.js",
3
- "version": "1.3.0",
3
+ "version": "1.4.0",
4
4
  "description": "Type-safe, lossless round-trip conversion between OpenDocument Format packages (odt, ods, odp) and JSON, hand-written and dependency-minimal, built on Zod 4 codecs.",
5
5
  "type": "module",
6
6
  "repository": {