@openpresentation/opf-pptx 0.13.2 → 0.15.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 +57 -37
- package/dist/annotation-export.js +1 -1
- package/dist/annotation-provenance.js +4 -0
- package/dist/background-import.js +33 -17
- package/dist/background.js +30 -28
- package/dist/body-text-import.js +16 -4
- package/dist/chart-alt.js +54 -0
- package/dist/chart-data.js +29 -54
- package/dist/chart-highlight.js +113 -0
- package/dist/chart-options.js +46 -24
- package/dist/chart-types.js +61 -75
- package/dist/code-provenance.js +20 -4
- package/dist/color-ref.js +12 -6
- package/dist/data-provenance.js +18 -14
- package/dist/document-provenance.js +208 -129
- package/dist/format-check.js +10 -0
- package/dist/furniture-provenance.js +30 -3
- package/dist/heading-provenance.js +10 -3
- package/dist/image-fallback-node.js +4 -1
- package/dist/image-provenance.js +273 -0
- package/dist/index.d.ts +57 -35
- package/dist/index.js +724 -399
- package/dist/media-provenance.js +3 -3
- package/dist/metric-provenance.js +3 -3
- package/dist/native-text-style.js +2 -0
- package/dist/package-fonts.js +2 -2
- package/dist/quote-provenance.js +70 -26
- package/dist/rich-heading.js +119 -0
- package/dist/run-colors.js +3 -0
- package/dist/script-fonts.js +102 -101
- package/dist/theme-colors.js +16 -11
- package/dist/timeline-provenance.js +17 -6
- package/dist/typeface-inventory.js +8 -8
- package/dist/watermark-provenance.js +65 -3
- package/package.json +4 -4
- package/dist/slide-image-provenance.js +0 -177
package/README.md
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
# OPF PPTX
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Catalogs (FA-23, OPF 0.15, unreleased; requires core 0.15): the package ships and reads no catalog data of its own. `toPptx(presentation, { catalogs })` and `fromPptx(bytes, { catalogs })` take core's registered catalogs (`Catalog[]`: `{ source, layouts?, themes?, colorSchemes?, fontSchemes?, ... }`, records keyed by id, never fetched), the same option opf-render and core's `resolveSlideContext` take. Every reference (slide `layout`, `design.theme`, `colorScheme`, `fontScheme`, a language object's font schemes) resolves through core: the document's embedded `catalogs.<group>` records first, then the registered catalog whose `source` the group names; the first registered catalog is the host default for bare ids, and `"default": false` closes that fallback. Register the gallery snapshot explicitly: `import { defaultCatalog } from "@openpresentation/opf/catalog"; toPptx(deck, { catalogs: [defaultCatalog] })`. A reference that resolves nowhere is reported once per path as core's `unresolved-reference` (kind, reference, path, group, source, fallback); the slide composes automatically and the theme and schemes draw with core's engine defaults (`ENGINE_DEFAULT_THEME`, `ENGINE_DEFAULT_COLOR_SCHEME`, `ENGINE_DEFAULT_FONT_SCHEME`; the theme part's colour scheme is then named `OpenPresentation`). `strictReferences: true` (default: the value of `strictAssets`) makes such a reference fail the export with `OPFPptxError` code `unresolved-reference`, its `path` and core's `diagnostics`. Removed with no alias: the per-source host record option, the `{kind: records[]}` form of `catalogs` (now refused with core's `OPFCatalogsOptionError`, code `invalid-catalogs`), catalog language ids, and catalog social-platform records (core formats socials from its `SOCIAL_PLATFORMS` vocabulary).
|
|
4
|
+
|
|
5
|
+
API names (RR-55, unreleased; requires core 0.14, and opf-render 0.14 for the optional peer): `toPptx(presentation, { fonts })` takes the fonts handle that opf-render's `loadFonts()` returns (`@openpresentation/opf-render/fonts-node`; core's `Fonts`, an object with at least `textMeasurement`) and no longer reads a top-level `textMeasurement`. `fromPptx(bytes, { signals: true })` resolves to `{ presentation, signals }` (it was `{ document, signals }`). `OPFPptxError` carries `findings` (the `error` findings of core's `format` check, or the variable errors) and `diagnostics` (the layout diagnostics of a `layout-overflow`), not `issues`. `inventoryPptxTypefaces` and `checkPptxTypefaces` are `inventoryTypefaces` and `checkTypefaces`, and `checkTypefaces` takes `{ families }` where it took `{ fonts }`. Each slide is composed with the options of core's `resolveSlideContext`, so the deck's resolved font families (slide design, deck design, theme, then the engine default) are the ones the layout is measured in; a reference that matches no record was reported per kind and never refused the export (0.15 replaces this with one `unresolved-reference` diagnostic and core's engine defaults; see Catalogs above): the default `minimal` theme and `cool-horizon` colour scheme were used, and a slide with no layout, or an unknown one, is composed with no layout record (automatic composition, as in the preview; it used to compose against the `blank` layout, so a layout-less cover is now centred vertically). The package no longer falls back when core lacks a function: it requires core 0.14.
|
|
6
|
+
|
|
7
|
+
Version 0.12.0 requires `@openpresentation/opf` ^0.12.0 (composed font sizes on PowerPoint's 0.01 pt grid, hanging wrap whitespace, promoted regions in reading order, right-to-left decks composed mirrored) and exports what that core composes, plus the export and import changes listed in the changelog; use it with opf-render 0.12.0 (optional peer ^0.12.0) and opf-editor 0.11.0 so preview and export resolve one core. Version 0.11.9 exports an SVG image as a native SVG picture over a PNG fallback (rasterized in Node by the optional `@openpresentation/opf-render` peer or `options.svgRasterizer`; without one it stays the placeholder), writes a deck footer's first text, date and slide number as native PowerPoint Header & Footer placeholders (output change for decks with a footer; every export now carries the footer placeholders on its master, layout and notes master; see [native header and footer](docs/native-header-footer.md)) and adds the opt-in `fromPptx(bytes, {signals: true})`, which also returns the raw per-shape signals of every slide (see Import signals; the option leaves the default import unchanged; core floor `^0.11.4` unchanged, no API removed). Version 0.11.8 re-imports rich text that wraps over several native lines as the one authored payload instead of reporting `content-structure-changed` (the export records the line count; decks exported by earlier versions import as before; core floor `^0.11.4` unchanged, no API change). Version 0.11.7 requires `@openpresentation/opf` ^0.11.4 and exports the design fields natively (cover and section logos, header and footer logos, picture bullets, the accent font), writes every chart's text at the size the preview draws, round-trips the slide content structure, sections, extensions, assets and brand images through PPTX provenance (a fresh export of a plain deck re-imports in its authored form, so read a root payload field before `blocks[0]`), writes slide sections as PowerPoint's section list, and degrades an SVG image to the preview's placeholder instead of aborting; install it with renderer 0.11.9 and editor 0.10.6 so export and preview resolve the same core (no API removed). Version 0.11.6 exports the treemap, histogram, pareto, box-and-whisker, waterfall and funnel charts as native chartex parts by default (`toPptx({chartex: 'auto'})`; `world` stays a clustered column with `chart-data-adapted`; pass `chartex: 'fallback'` for the previous output) and gives chartex text the deck's label colour and font (no API removed). Version 0.11.5 writes the slide tag run as a theme color reference (`a:schemeClr accent1`) where the deck theme holds the primary there (no API change; the color is unchanged). Version 0.11.4 re-imports quote payloads (each native quote line carries an `OPF_QUOTE_V1` tag) and slide-level image payloads from an unchanged export, writes the theme's `a:ea` and `a:cs` only for slots a script font is selected for, and requires core `^0.11.3` (the 70 legacy gallery layout ids and their geometry; install it with renderer 0.11.6 and editor 0.10.4 so export and preview resolve the same core; no API removed). Version 0.11.3 adds the opt-in `toPptx({ chartex: 'native' })` export of the seven chartex chart types and always imports chartex charts (no API removed; the default output is unchanged). Version 0.11.2 exports the classic chart types as native chart constructs and writes named and default table colours, text pairs and hyperlink colours as theme references (no API or dependency change; resolved colours are unchanged). Version 0.11.1 exports `design.watermark` as a native picture (it re-imports) and embeds byte-identical media parts once, with no API or dependency change. Version 0.11.0 requires core `^0.11.2` (cover slides are centered between header and footer furniture) and the optional `@openpresentation/opf-render` peer `^0.11.0`; install them together so export and preview resolve the same core. Version 0.10.0 required core `^0.11.1` and the peer `^0.10.0`. It adds native slide-number and date fields, socials, slide-level image treatments, theme color schemes, script-slot fonts and RTL, re-import of design and metadata references, native underline and current-body formatting on import, and UTC-canonical `zipDate` (see the changelog for the intentional contract changes).
|
|
4
8
|
|
|
5
9
|
Chart options (RR-35, unreleased): `axisTitles`, `legend` and `dataLabels` on a chart export as native `c:title`, `c:legendPos` and `c:dLbls` (and `cx:title`, `cx:legend`, `cx:dataLabels` on the Office 2016 constructs) and import back; the preview draws the same options. See core `docs/chart-options.md` for the per-type support table and the `chart-option-adapted` diagnostic.
|
|
6
10
|
|
|
7
|
-
Chart
|
|
11
|
+
Chart emphasis (FA-14, unreleased): `chart.highlight` exports as native series fills (`c:ser/c:spPr`, plus `c:marker/c:spPr` on line, scatter and radar series) and per-point fills (`c:dPt`: `c:spPr` on a column or bar, a circular `c:marker` on a line, the slice fill on a pie or doughnut). The accent is `a:schemeClr accent1` when the deck theme holds the primary exactly (a theme edit in PowerPoint recolors it) and an sRGB literal when the contrast adjustment moved it; the muted color is an sRGB literal (a mix of two theme colors is not a scheme color). Labels inside a mark take the color that contrasts with that mark. The highlight is not inferred on import: in `full` provenance mode it is recorded in the chart's `OPF_DATA_V1` record and restored while the chart's cached data is unchanged. Chartex constructs are not highlighted. See core `docs/chart-options.md`.
|
|
12
|
+
|
|
13
|
+
Chart and table data (RR-54, unreleased; core `docs/chart-table-data.md`): chart values pass core `chartNumber` (a string that is not a plain decimal, such as `"12%"`, `"$5"` or `"(5)"`, is a gap and one `chart-value-not-numeric` diagnostic per chart, where the exporter used to strip it to a number). A column number format (`{"name": "Revenue", "format": "$#,##0.0"}`) is written as the series format code, the data-label and value-axis number formats (the axis takes the first plotted series' format, as the preview does) and the embedded workbook cell formats (Edit Data shows it), on classic and chartex charts. Top-level `datasets` referenced by `{"dataset": "<id>", "fields"?}` and `chart.mapping` export exactly like their inline equivalent; dataset tables and formatted table cells export their display text. With `full` provenance, `OPF_DATASETS_V1` (beside the document tag) and `OPF_DATA_V1` (on each chart or table frame that uses these fields) restore the datasets, references, mapping, formats and `data.source` on import while the native caches (a plain PowerPoint save keeps them), the referenced datasets and the cell texts are unchanged (tags are limited to 16 MiB and shape-checked; a bad one is reported as `invalid-data-provenance`); a deck without them maps every series format code that core `fromExcelNumberFormat` maps back to the column format and reports any other code (`chart-number-format-adapted`). A deck that uses none of these fields exports byte for byte as before.
|
|
8
14
|
|
|
9
15
|
Version 0.9.1 kept core `^0.11.0` and raised the optional `@openpresentation/opf-render` peer to `^0.9.0` so it coexists with editor 0.8.0. ColorRef / `variables` on content colors still hex-resolve through core `resolveColorRef()` before PptxGenJS `srgbClr` export. Unrecognized run colors such as `color:'invalid'` still validate and fall back to the theme text color. Native DrawingML `schemeClr` and theme `clrScheme` writes, and native `p:hf` headers/footers, are not in this release. Import still flattens theme colors to hex. Metric, quote and timeline layout placeholders and the corrected text-bullet contract from 0.8.1 are retained.
|
|
10
16
|
|
|
@@ -61,13 +67,13 @@ await fs.promises.writeFile("quarterly-review.pptx", bytes);
|
|
|
61
67
|
|
|
62
68
|
Embedded picture bytes are a PNG, JPEG, GIF, WebP or SVG. Bytes that are no readable image (or an SVG that cannot be exported: malformed XML, no `xmlns`, no width and height and no viewBox, or no rasterizer, see below) export what the preview shows, like an unresolved asset: the "Image unavailable" placeholder (nothing for a watermark, the background colour for a background image) and one `unresolved-asset` diagnostic with a `reason` (`unsupported-format`, `svg-malformed`, `svg-no-size`, `svg-too-large`, `svg-unsafe`, `svg-unreadable`, `svg-rasterizer-unavailable`, `svg-render-failed`); `strictAssets` throws `unsupported-image-dimensions` for an unreadable raster, `invalid-svg-image` for an unusable SVG, `svg-rasterizer-unavailable` or `svg-render-failed`.
|
|
63
69
|
|
|
64
|
-
SVG pictures are native SVG pictures, as PowerPoint 2016 and Microsoft 365 store them: a `p:pic` whose `a:blip` embeds a PNG fallback raster and carries the `asvg:svgBlip` extension that points at the SVG media part (`image/svg+xml`, content type registered). PowerPoint 2016+ draws the SVG (crisp at any zoom; Graphics Format, Convert to Shape); older viewers draw the PNG. This covers
|
|
70
|
+
SVG pictures are native SVG pictures, as PowerPoint 2016 and Microsoft 365 store them: a `p:pic` whose `a:blip` embeds a PNG fallback raster and carries the `asvg:svgBlip` extension that points at the SVG media part (`image/svg+xml`, content type registered). PowerPoint 2016+ draws the SVG (crisp at any zoom; Graphics Format, Convert to Shape); older viewers draw the PNG. This covers image blocks (placed or not), the alt-text picture of an image background, the watermark, header and footer images and logos, and cover and section logos. The picture takes the same frame and fit (`cover`, `contain`, `stretch`) as a raster of the same proportions; the SVG's size is its `width` and `height` (any unit), else its `viewBox`. Exceptions, each a raster only: a picture bullet (`a:buBlip` can reference a raster only, so it is the PNG), an image background without alt text (a background picture fill carries no SVG), a tiled background picture, and an image block with a duotone recolor or a non-rectangular shape (the effect is written on the PNG blip, so it applies as in the preview, and `svg-image-rasterized` is reported). Opacity (a translucent watermark, slide image opacity), grayscale and the slide image border stay native SVG pictures: PowerPoint applies them to an SVG picture.
|
|
65
71
|
|
|
66
72
|
The PNG fallback is drawn at 192 dpi of the displayed size (a multiple of 128 px on its long side, 128 to 2304 px, transparent background) by `options.svgRasterizer(svg, {width, height, scale, text})`. In Node the default is opf-render's `svgToPng` (resvg with the bundled fonts and no system fonts, so the same SVG gives the same bytes on every machine), loaded only when an SVG is exported: it is the optional peer `@openpresentation/opf-render`. Without opf-render and without `svgRasterizer` (a browser build has no default; pass one that draws the SVG deterministically), the SVG picture is the placeholder with `reason: "svg-rasterizer-unavailable"`. An `imageResolver` that returns a raster for an SVG source still gives a plain raster picture, and one that returns SVG bytes (`mediaType: "image/svg+xml"`) gives a native SVG picture. A local `.svg` path is read in Node.
|
|
67
73
|
|
|
68
74
|
An SVG is never run or fetched. It is validated as XML and sanitized before it is embedded (and on import): `<script>`, `<foreignObject>`, event-handler attributes, link animations, every `href`/`url()` that is not a `#fragment` or an inline PNG, JPEG, GIF or WebP, `@import` rules, `xml-stylesheet` instructions and DOCTYPE declarations are removed (a DOCTYPE entity that is plain text is expanded; an external or nested entity is refused as `svg-unsafe`), and `svg-sanitized` reports what was removed. An SVG that needs none of that is embedded byte for byte, so import returns the authored source.
|
|
69
75
|
|
|
70
|
-
`fromPptx` reads a picture that carries `asvg:svgBlip` back as the SVG (a `data:image/svg+xml;base64,` source), not its PNG fallback, from this exporter's files and from PowerPoint's (a damaged or hostile SVG part imports its PNG fallback and reports `invalid-svg-image`, or imports sanitized with `svg-sanitized`). Design fields with SVG sources (`design.logo`, `design.watermark`,
|
|
76
|
+
`fromPptx` reads a picture that carries `asvg:svgBlip` back as the SVG (a `data:image/svg+xml;base64,` source), not its PNG fallback, from this exporter's files and from PowerPoint's (a damaged or hostile SVG part imports its PNG fallback and reports `invalid-svg-image`, or imports sanitized with `svg-sanitized`). Design fields and blocks with SVG sources (`design.logo`, `design.watermark`, image blocks with their treatments) round-trip as SVG.
|
|
71
77
|
|
|
72
78
|
`fromPptx` parses an existing `.pptx` buffer locally and returns an OPF document that validates with `@openpresentation/opf`:
|
|
73
79
|
|
|
@@ -95,12 +101,13 @@ The first exporter keeps the public API stable while using `pptxgenjs` internall
|
|
|
95
101
|
- `Slide.title`, `Slide.subtitle`, and `Slide.tag` become editable text boxes, not PowerPoint master placeholders.
|
|
96
102
|
- Root payloads, `blocks[]`, and promoted region keys become editable slide objects in deterministic regions. Promoted keys use the OPF 3x3 region vocabulary (`top`, `middle`, `bottom`, `left`, `center`, `right`).
|
|
97
103
|
- Text, lists, metrics, quotes, timelines, code, tables, and inline-data charts are emitted as editable PowerPoint text, table, and chart objects. Content ColorRef values (hex, scheme slots/roles, and `var:<id>`) resolve through core `resolveColorRef()`. Slot and role names become native `a:schemeClr` references when the exported theme holds that exact color; hex, `var:<id>` and slide-override colors stay `a:srgbClr` (see [Theme color scheme](#theme-color-scheme)). Each classic chart type the core catalog keeps (one per Aspose.Slides `ChartType`: clustered/stacked/100% stacked column and bar, line and stacked line with or without markers, area/stacked/100% area, pie, doughnut, scatter with markers, radar/radar with markers/filled radar) is written as its exact native construct; deprecated core ids export as their replacement; other ids keep the previous best-effort mapping. The chartex types treemap, histogram, pareto, box & whisker, waterfall and funnel are written as native Office 2016 chartex parts (confirmed in desktop PowerPoint on 2026-09-30; see [Chartex charts](#chartex-charts-ff-22b)); the map (`world`) stays a clustered column chart by default and reports `chart-data-adapted` (`chartex-fallback`) until PowerPoint accepts its regionMap part (`chartex: "native"` writes it anyway, `chartex: "fallback"` writes clustered columns for every chartex id). A pie or doughnut chart, or a one-series chartex construct, plots only its first series and reports `chart-data-adapted` (`series-dropped`) when it has more.
|
|
104
|
+
- A timeline event's `status` (`done`, `current`, `planned`) is drawn from the deck's colors with core's shared shapes (the same as the preview): a filled `ellipse` marker for `done` and for no status, a `current` marker inside a ring (a second `ellipse`, tagged with its marker) with a bold label, and a `planned` marker that is an `ellipse` with the background fill and an accent outline, with muted text kept at 4.5:1 or more. Import restores `status` from the marker tags, not from the shapes' look, and drops the timeline provenance (keeping plain text) when a current event's ring is missing or duplicated.
|
|
98
105
|
- A chart whose data is one column of values (a histogram or dot plot) has no category column. A histogram or Pareto chart bins the values itself (PowerPoint's histogram with an explicit automatic bin count, no diagnostic) and re-imports as authored; any other chart type plots the values against their row numbers and reports `chart-data-adapted` (`row-numbers`). With `chartex: "fallback"` a histogram is binned into equal-width bins (Sturges' count, at most 50) and written as a column chart of the counts (`histogram-binned`; the binned counts do not round-trip). Cells parse as in every chart (core `chartNumber`: numbers and plain decimal strings; "12%", "$5" and "1,234" are not numbers) and cells that hold no number are skipped, not plotted as 0. Chart data that cannot be plotted (no numbers, no rows, or an external data source), an empty table and unsupported content keep a placeholder frame with a plain-language description (never a dump of the source data or URLs) and report `chart-data-unplottable` or `content-placeholder` with a `reason`; content is never dropped without a diagnostic.
|
|
99
106
|
- Image assets are embedded only when supplied as data URIs, local paths, or host-resolved bytes/paths. Remote asset URLs are never fetched by the runtime path. A picture's native description is the authored `alt` (never a stand-in such as `preencoded.png` or the file path), and a distinct asset `title` is the picture's native `title`; `fromPptx` reads both back.
|
|
100
107
|
- A chart cell that holds no number (`null`, an empty or non-numeric string, a boolean) is a gap in the native chart: its cache has no point at that row (`c:ptCount` keeps the row count), its workbook cell is blank, and it re-imports as `null`. An actual zero stays zero.
|
|
101
108
|
- ZIP entries, generated chart/workbook part names, core-property timestamps, and nested chart workbook timestamps are normalized for reproducible bytes. [Export determinism](docs/export-determinism.md) lists the tested time zone, locale, clock and host-font controls and each known variance (WebP conversion, font registries, ICU segmentation, timestamps).
|
|
102
109
|
- The package names only the document's chosen fonts. Chart text (data labels, axes, legend, titles) uses the chart slide's body font in `latin`/`ea`/`cs`; each embedded chart workbook uses the same fonts in its styles and theme; run `pitchFamily` follows the font scheme type (monospace is fixed pitch, serif is roman); and `docProps/app.xml` "Fonts Used" lists the fonts the package actually uses. The theme keeps PptxGenJS's per-script supplements (`THEME_SCRIPT_SUPPLEMENTS`) and empty `ea`/`cs` slots unless a script font was selected for that slot (see [Languages, right-to-left text and script fonts](#languages-right-to-left-text-and-script-fonts)).
|
|
103
|
-
- `
|
|
110
|
+
- `checkTypefaces(bytes, {families, monospace, themeScripts, themeScriptsByPart})` (optional `themeScripts: {major: {ea, cs}, minor: {ea, cs}}` names the East Asian / complex-script families the author selected; a selected theme slot that is empty or different is reported as `theme-script-slot`, an unselected one may stay empty; `themeScriptsByPart` overrides it per theme part, for the theme of a per-slide script master; theme references resolve against the theme of the part's own slide or notes master) inventories every `typeface`, workbook font name and "Fonts Used" entry in every XML part, including nested packages, and reports each font outside that policy. `inventoryTypefaces()` returns the raw inventory.
|
|
104
111
|
|
|
105
112
|
This pass did not require an OPF schema change. The deferred full OOXML placeholder mapping from `docs/plans/layout-placeholders.md` remains a later hand-written OOXML emitter concern.
|
|
106
113
|
|
|
@@ -118,10 +125,12 @@ This intentionally tightens the previous host-dependent `Date` parsing contract.
|
|
|
118
125
|
|
|
119
126
|
### Quote provenance (FF-57)
|
|
120
127
|
|
|
121
|
-
A `quote` payload exports as native text lines: the body (the quoted text wrapped in straight quotation marks) and, when there is an attribution or source, one footer
|
|
128
|
+
A `quote` payload exports as native text lines: the body (the quoted text wrapped in straight quotation marks) and, when there is an attribution, role or source, one footer: the attribution, the `role` on its own line below it, and the source after ` - ` on the last line. Each line shape carries an `OPF_QUOTE_V1` tag that stores only topology (whether the value was the string shorthand, which footer fields exist, where each separator falls, whether the quote has a photo, and how many source lines each part has). No quote word is stored: every value is read back from the current native text, so a cleared or edited quote cannot bring back the words it once held.
|
|
122
129
|
|
|
123
130
|
An unchanged export re-imports as `{ "type": "quote", "quote": ... }` exactly: the string shorthand stays a string, `text`/`attribution`/`source` come back with their whitespace, hard line breaks and inner quotation marks, and several quotes on one slide keep their order. Native edits to the body or footer text import as the edited quote. The importer reports `quote-import-reflow` (native formatting, position and font theme are not reconstructed) and `quote-footer-merged` when an edited footer no longer separates attribution from source. A missing, duplicated or reordered line, a changed manifest or a native bullet on a tagged line rejects the group: the shapes import as ordinary text blocks with an `invalid-quote-provenance` diagnostic and no old words are restored. A quote authored without tags (for example in PowerPoint) imports as text. The output stays ordinary editable PowerPoint text boxes; the tags are `p:custDataLst` entries that PowerPoint keeps.
|
|
124
131
|
|
|
132
|
+
A quote with a `photo` (FA-12) also exports the headshot as one native picture in the circle frame core computes beside the footer lines: cropped to fill it (`a:srcRect`), with the `ellipse` preset geometry, the asset's alt text as the picture description, and the same `OPF_QUOTE_V1` group tag (role `photo`, no words). Import restores `role` from the footer and `photo` (`src`, `alt`) from the current picture; a picture that was deleted imports the quote without a photo and reports `quote-photo-missing`, and a renamed or duplicated one rejects the group like a damaged text line. An SVG photo exports as its PNG raster (`svg-image-rasterized`), because the circular mask is not confirmed on an SVG picture in PowerPoint. An unresolved photo source draws the ordinary image placeholder.
|
|
133
|
+
|
|
125
134
|
### New in 0.10.0: current native body formatting
|
|
126
135
|
|
|
127
136
|
Current source imports supported formatting from ordinary untagged native body text and list items. A value that previously imported as a string can now be a rich-run array containing the same current characters with explicit native properties. Unstyled values remain strings; title/subtitle selection and tagged recovery stay separate. Run, field and break order, blank paragraphs, significant whitespace, explicit normal overrides, paragraph/list defaults, point sizes, Latin font families, supported colors/alpha, hyperlinks and script direction come from the current PPTX. Edits, clears and deletion remain authoritative in every provenance mode. Native weight faces of the bundled Roboto family (for example `Roboto SemiBold`) import as `Roboto` plus `bold` for weights of 600 and above. Medium (500) and lighter weights import as regular, because OPF runs have no numeric weight, so a round trip re-exports Medium as Regular; `approximate-body-font-weight` reports this. Authored families such as `Arial Black` or `Aptos Light` keep their names.
|
|
@@ -131,19 +140,20 @@ This does not reconstruct original source run identities or boundaries between n
|
|
|
131
140
|
The first importer is mechanical and schema-compatible:
|
|
132
141
|
|
|
133
142
|
- Presentation core properties map to OPF `name`, `description`, and `author`.
|
|
134
|
-
- The first slide master's theme `clrScheme` maps to `design.colorScheme`: a catalog
|
|
143
|
+
- The first slide master's theme `clrScheme` maps to `design.colorScheme`: the bare id of a colour scheme in the host default catalog (the first of `fromPptx`'s `catalogs`) on an exact twelve-slot match, otherwise inline slots. A theme named like a theme of that catalog maps to `design.theme` when its colors or heading/body fonts corroborate it. With no catalog registered the colour scheme is inline and no theme id is recovered (see [Theme color scheme](#theme-color-scheme)).
|
|
144
|
+
- Rich headline text (FA-10): a `TextRun[]` `title`, `subtitle` or `tag` exports as one tagged native text box per laid-out line with its runs (color, weight, italic, links, super/subscript, citation markers), and a `TextRun[]` `quote.text` the same way under the quote tags. Import returns `TextRun[]` where a heading's native runs are formatted differently from one another and a plain string where they are uniform: a tagged export records the heading's own weight, color, size and family (`base`), so only authored run formatting comes back, and a native title placeholder is read like body text, dropping what every run shares. A weight that the export writes as bold on every run of a title (and on a quote body) is the default, so only a `bold: false` word in a title is kept; `bold: true` in a quote body is indistinguishable from the body weight and is not restored. Marker runs become `cite`/`footnote` again, and heading and quote runs take part in the run-color reference slots.
|
|
135
145
|
- Native title/subtitle placeholders retain their roles. On slides without complete OPF heading tags, recognizable text-box positions and sizes provide a fallback. If any complete OPF heading role is recovered, untagged body text stays in `blocks[]` instead of being promoted into an absent heading role. Damaged tags retain visible text through ordinary import and diagnostics.
|
|
136
146
|
- Remaining text boxes map to `blocks[]` as text or list payloads, sorted by OOXML position.
|
|
137
|
-
- PowerPoint tables map to OPF table blocks, embedded images map to data URI image blocks, and cached chart series map to OPF chart blocks whose `type` is the core chart type id for the native construct (for example a `percentStacked` column chart imports as `100pct-stacked-column
|
|
147
|
+
- PowerPoint tables map to OPF table blocks, embedded images map to data URI image blocks, and cached chart series map to OPF chart blocks whose `type` is the core chart type id for the native construct (for example a `percentStacked` column chart imports as `100pct-stacked-column`, a filled radar as `filled-radar`; a scatter chart imports as `[Point, X, ...series]`).
|
|
138
148
|
- Chart cache points are placed by their native `c:pt@idx`, with `c:ptCount` retaining trailing gaps. Missing labels and values become `null`; explicitly empty labels and series names stay empty strings, and an actual numeric zero stays zero. Empty numeric cache values remain missing. Malformed or duplicate indices, contradictory counts, competing caches and allocation limits reject with `invalid-chart-cache` and the chart-part/cache path; hierarchical category caches reject with `unsupported-chart-cache` instead of flattening labels. Each cache is limited to 100,000 positions, and combined caches and the emitted table each to 1,000,000 cells. Complete decimal scientific notation in numeric caches is read as a number; exponent overflow or nonzero underflow to zero rejects with `unsupported-chart-cache` and the chart part, series, cache and logical point path. Zero coefficients remain zero. This finite-number boundary does not classify rejected text as invalid OOXML. Malformed exponent strings and other nonblank numeric text retain the existing legacy conversion policy. This cache-only import does not repair embedded worksheets, restore unsupported scatter X/point labels, change exporter/workbook parsing or export/re-export missing-data handling, or establish native chart fidelity.
|
|
139
149
|
- Table imports retain empty rows. A native `firstRow` flag of `1` or `true` maps the first row to column labels; absent/false flags retain every row as data. New exports set this flag from OPF columns. Older exports without the flag retain their labels as the first data row rather than inferring headers.
|
|
140
150
|
- Native table text preserves run/field/break order, significant whitespace, and blank paragraphs. Cells return canonical `{value, style}` objects; `value` retains scalar text or supported rich runs. Covered merge positions are `null`. Explicit normal headers override OPF’s bold header default. Numeric/boolean/null source types cannot be reconstructed from native display text.
|
|
141
|
-
- Run `lang` maps back to the presentation `language`
|
|
151
|
+
- Run `lang` maps back to the presentation `language` as a BCP-47 tag (the run's own tag, for example `ja-JP`); see [Languages, right-to-left text and script fonts](#languages-right-to-left-text-and-script-fonts).
|
|
142
152
|
- Imported runs retain bold, italic, underline, strike, point sizes, Latin font families, solid colors/alpha, external hyperlink URLs, and superscript/subscript direction. List-level and paragraph defaults apply before run overrides; supported theme fonts/colors resolve from the archive. Field values become their cached text, and underline/strike variants and baseline magnitudes reduce to OPF booleans.
|
|
143
153
|
- Citations, footnotes and captions (core RR-34 fields): a marker exports as a native superscript run (`baseline="30000"`) after its run; the slide's footnote area is a line shape plus one tagged text box per listed line (`OPF_FOOTNOTES_V1`), a caption one tagged text box per line (`OPF_CAPTION_V1`) naming its media shape, both at core's geometry; `references` travel in the document record. Import re-attaches tagged captions to their media block, rebuilds `references`, `cite` and `footnote` from the footnote tags and the marker runs (removing the marker runs; an edited note keeps its edited text), and guesses nothing without tags: a superscript number stays a superscript run and a text box stays a text block.
|
|
144
154
|
- Conditional table styles, merged-cell geometry, cell fills/borders/alignment, unsupported text fills/colors, and internal hyperlink actions are not fully reconstructed. `onDiagnostic` reports unsupported table style references, merges, fonts/colors/fills and links with native frame/cell paths. Table paths use the native graphic-frame and row indexes, including a header row. The shared core 0.6.0 layout sizes rows from their content and reports `text-overflow` when text cannot fit at the minimum size. These checks establish native XML conversion, not visual parity with PowerPoint.
|
|
145
155
|
- Unknown non-text shapes and unsupported graphic frames become editable text fallback blocks instead of failing the import.
|
|
146
|
-
- Catalog references (`design.theme`, `colorScheme`, `fontScheme`, `dimensions`, `background`, slide `layout`), slide ids and authoring metadata (`narrative`, `tone`, `audience`, `purpose`, `language`, `organization`, `speaker`, ...) are stored at export in `OPF_DOCUMENT_V1` / `OPF_SLIDE_V1` customer-data tags. Import restores a reference while the theme colors, theme fonts, slide size, background or slide arrangement it produced are unchanged. After an edit, the observed native values stay and `design-reference-changed` / `layout-reference-changed` name the reference. `toPptx` option `provenance: 'references-only' | false` limits or disables these invisible tags. Slide layout intent (layout
|
|
156
|
+
- Catalog references (`design.theme`, `colorScheme`, `fontScheme`, `dimensions`, `background`, slide `layout`), slide ids and authoring metadata (`narrative`, `tone`, `audience`, `purpose`, `language`, `organization`, `speaker`, ...) are stored at export in `OPF_DOCUMENT_V1` / `OPF_SLIDE_V1` customer-data tags. Import restores a reference while the theme colors, theme fonts, slide size, background or slide arrangement it produced are unchanged. After an edit, the observed native values stay and `design-reference-changed` / `layout-reference-changed` name the reference. `toPptx` option `provenance: 'references-only' | false` limits or disables these invisible tags. Slide layout intent (layout reference, type, composition, composition hints and the embedded layout record with its catalogs group and source) lives in each `OPF_SLIDE_V1` record, so a slide keeps its layout even without the document tag, for example when it is pasted into another deck (FF-29). A layout that resolves only in a registered catalog stores no record and restores where `fromPptx` is given that catalog. See [document round trips](docs/document-roundtrip.md) for exactly what is embedded.
|
|
147
157
|
- The slide's content structure is part of `OPF_SLIDE_V1` too (`full` mode): nested `group` blocks, promoted regions (`left`, `top:left`, ...), a root payload (`text`, `items`, `chart`, ...) versus `blocks`, block `id`s and `extensions`, group `composition`, with the reference-pixel box of every leaf from the same composition the export drew. Import matches the native shapes to those boxes and rebuilds the authored form while the slide's arrangement is unchanged, so `slide.type` validates again; a slide whose blocks no longer fit the stored boxes keeps its flat blocks and reports `content-structure-changed` at `slides.N`; a duplicated slide reports `duplicate-block-id` and drops the repeated id. The document tag also stores `filename`, root `extensions`, slide `section` and `extensions`, `design.logo` (deck and slide) and the whole `assets` registry; a `data:` source that is not an exported picture is stored inline up to 256 KiB, so an organization logo or speaker photo given as a data URI round-trips.
|
|
148
158
|
- Slide `section` labels are written as PowerPoint's native section list (`p14:sectionLst` in `ppt/presentation.xml`, whatever the `provenance` option): one section per run of consecutive slides with the same label, runs without a label as `Default Section`, with deterministic ids. Import reads the list back (`Default Section` means no section); it wins over the stored value and over the section text a footer shows (`section-reference-changed` at `slides.N.section`), except that an edited footer line keeps its text while the list still equals the stored value. The native PowerPoint check of the sections pane and save/reopen is a separate gate (`scratchpad/spec-gaps-native/`).
|
|
149
159
|
|
|
@@ -156,15 +166,15 @@ A deck exported by this package carries OPF provenance tags, so its structure re
|
|
|
156
166
|
```js
|
|
157
167
|
import { fromPptx } from "@openpresentation/opf-pptx";
|
|
158
168
|
|
|
159
|
-
// Default: the
|
|
160
|
-
const
|
|
169
|
+
// Default: the presentation.
|
|
170
|
+
const presentation = await fromPptx(bytes);
|
|
161
171
|
|
|
162
|
-
// Opt in: the same
|
|
163
|
-
const {
|
|
172
|
+
// Opt in: the same presentation, and a signals value beside it.
|
|
173
|
+
const { presentation: same, signals } = await fromPptx(bytes, { signals: true });
|
|
164
174
|
// or with limits: fromPptx(bytes, { signals: { maxSlides: 100, maxShapesPerSlide: 150 } })
|
|
165
175
|
```
|
|
166
176
|
|
|
167
|
-
Without `signals` (or with `false`) the return value and the document are byte for byte what they were; with it the
|
|
177
|
+
Without `signals` (or with `false`) the return value and the document are byte for byte what they were; with it the presentation is identical and the result is `{presentation, signals}`. Invalid limits throw `OPFPptxError` with code `invalid-signals-option` before any work. `signals` is plain JSON (`JSON.stringify` and back loses nothing), the same bytes in always give the same signals, and the types are `PptxSignals` in `index.d.ts`.
|
|
168
178
|
|
|
169
179
|
**What it reports.** `signals.deck` has the slide size (EMU and reference px, 96 per inch), the theme (colour and font scheme names, major and minor font, the twelve colour slots) and whether the deck carries OPF tags. `signals.slides[i]` has the part, layout (`name`, `type`, part), master (part, name, theme name), whether the slide is hidden, whether it carries an OPF slide tag, and `shapes`: every shape in z-order (document order, back to front; `zOrder` 0 is the back), each with:
|
|
170
180
|
|
|
@@ -174,12 +184,16 @@ Without `signals` (or with `false`) the return value and the document are byte f
|
|
|
174
184
|
- `text`: paragraphs with `level`, `align`, the effective `bullet` (inherited from the list style when the paragraph states none), indents and spacing, and runs with `font`, `size`, `bold`, `italic`, `weight`, `underline`, `strike`, `baseline`, `caps`, `color` (`#RRGGBB`) with its theme `colorRef`, `monospace`, `link`, `field` and `lineBreak`. Values are the effective ones PowerPoint draws: the run, its paragraph, the shape's list style, the layout and master placeholder, the master text styles, the presentation defaults and the theme (font tokens such as `+mj-lt` and scheme colours are resolved). A value no part of that chain states is omitted, and defaults are omitted (`bold`, `italic`, `monospace` absent means false, `weight` absent 400, `level` absent 0, `align` absent left). `monospace` is true for a known fixed-width family (Consolas, Courier New, Menlo, JetBrains Mono, ...); `text.monospaceShare`, `dominantFont`, `dominantSize`, `maxFontSize`, `anchor` and `autofit` summarise the shape.
|
|
175
185
|
- `fill`, `outline` (kind, colour, width, dash, arrowheads) and `geometry` (`rect`, `roundRect`, `ellipse`, ...). Fills and lines that only name a theme style are reported as `kind: "style"` with the style index and colour.
|
|
176
186
|
- `picture` (part, media type, bytes, natural size, crop), `table` (rows, columns, widths, header flags, cell text), `chart` (chart types, series and point counts, title).
|
|
177
|
-
- `opf`: where the importer put the shape in the document it returned. `{role: "block", path: "slides.2.blocks.1", blockType: "text"}`, `{role: "title", path: "slides.0.title"}`, or `null` when the importer made nothing of it (a connector, a table inside a group). For an OPF export the path follows the rebuilt structure (`slides.0.blocks.0.blocks.1`, `slides.1.left`, `slides.2.text`). A shape the importer consumed without a block of its own has a `role` only (`furniture`, `logo`, `watermark`, `
|
|
187
|
+
- `opf`: where the importer put the shape in the document it returned. `{role: "block", path: "slides.2.blocks.1", blockType: "text"}`, `{role: "title", path: "slides.0.title"}`, or `null` when the importer made nothing of it (a connector, a table inside a group). For an OPF export the path follows the rebuilt structure (`slides.0.blocks.0.blocks.1`, `slides.1.left`, `slides.2.text`). A shape the importer consumed without a block of its own has a `role` only (`furniture`, `logo`, `watermark`, `background`, `image-overlay`, or the member of a tagged group). A group links where all its members do. Several shapes can link to one block (the lines of a list, the parts of a quote).
|
|
178
188
|
|
|
179
189
|
**Bounds.** The output is bounded by `signals` limits, reported in `signals.limits` (defaults, with the largest value each accepts): `maxSlides` 300 (5000), `maxShapesPerSlide` 300 (5000), `maxTotalShapes` 5000 (100000), `maxParagraphsPerShape` 200 (5000), `maxTextCharsPerShape` 8000 (200000), `maxTextCharsTotal` 400000 (5000000), `maxTableCells` 400 (10000), `maxTableCellChars` 200 (5000), `maxGroupDepth` 12 (32). What a bound drops is counted: `signals.truncated` (slides not reported, text budget exhausted), `slide.truncated` (shapes, depth, paragraphs, text, table) and `paragraph.truncated`. `text.chars` and `text.paragraphCount` are the true counts. Images are never embedded, only described, so the size follows the text: about 0.7 KB of JSON per shape.
|
|
180
190
|
|
|
181
191
|
**Not claims.** Signals are facts, not conclusions: a `monospace` run is not a code block, and `opf: null` is not an error. They are read from the package, not rendered: the effective values follow the OOXML inheritance rules, not PowerPoint's layout engine (text wrapping and autofit are not computed, a group's rotation is not applied to its members' boxes, and an unresolvable colour or font is left out). They describe the upload, which can be hostile: the text is the deck's text, and a host that passes it on to a model should treat it as untrusted data, never as instructions.
|
|
182
192
|
|
|
193
|
+
## Combo charts (FA-15)
|
|
194
|
+
|
|
195
|
+
`type: "combo"` is exported as one classic chart part: one `c:plotArea` with a clustered column `c:barChart` on the primary axes and a `c:lineChart` (markers, `c:grouping standard`) for the line series. Lines named in `chart.secondaryAxis` form a second `c:lineChart` on a second `c:valAx` (`axPos r`, `crosses max`, its first series' number format, no gridlines, `axisTitles.secondary` as its `c:title`) and a deleted second `c:catAx`. Core's `resolveChartData` orders the series (columns, then primary-axis lines, then secondary-axis lines), each keeps its palette colour by its place in the whole chart, the embedded workbook holds every series, and a gap breaks a line (`c:dispBlanksAs gap`) as in the preview. The line groups are rewritten into `dml-chart.xsd` order (`c:grouping` first, no `c:invertIfNegative`, `c:marker` before `c:dLbls`). Import reads a plot area with one clustered column group and one or two line groups as `combo`, with `line` (only when it is not the default last series), `secondaryAxis`, the secondary axis title and the data label position; the authored column order comes back through the chart's data record.
|
|
196
|
+
|
|
183
197
|
## Chartex charts (FF-22b)
|
|
184
198
|
|
|
185
199
|
**Modes.** `toPptx(document, {chartex})` takes `"auto"` (default), `"native"` or `"fallback"`. The native check of 2026-09-30 (desktop PowerPoint, read-only, the program's FF-22b deck set) opened every chartex deck without a repair prompt and reported the native `Chart.ChartType` for treemap (117), histogram (118, by value and by category), pareto (122), box-and-whisker (121), waterfall (119) and funnel (123); for `world` PowerPoint took the `mc:Fallback` (51, clustered column) when the choice required `cx5`. The recheck of the same day showed that PowerPoint accepts the regionMap part when the choice requires `cx4` (the 2016/5/10 region-map namespace; `cx3`, `cx4` and `cx6` reported ChartType 140, `cx5` and `cx8` fell back) but, with no `cx:geoCache`, shows "There was a problem getting the information for your map chart" and draws nothing until it can fetch map data online. Supervisor decision: `"auto"` writes the chartex parts for the six confirmed constructs and keeps `world` on the clustered column chart, which always renders, with `chart-data-adapted` (`chartex-fallback`); `"native"` writes every chartex part, the map included with `Requires="cx4"` and the `chart-map-geodata` diagnostic (provider data is never fabricated); `"fallback"` writes clustered columns only, byte for byte as before FF-22b (`test/fixtures/chartex-fallback-main.json` pins it, with `chartex-fallback` and `histogram-binned`). The same check showed chartex labels drawn in the chart style's theme grey and box lines in black on a dark theme, so every chartex text element and the style part now carry the deck's label colour and font explicitly, and box series carry a line in the label colour. Import of chartex parts is always on.
|
|
@@ -202,7 +216,7 @@ The chart area, label colour, label font (`latin`/`ea`/`cs` in the chart's body
|
|
|
202
216
|
|
|
203
217
|
The exporter reads the presentation `language` through core `resolveScriptFonts()` (FF-07; the model is core `docs/programs/font-fidelity-everywhere/script-font-model.md`):
|
|
204
218
|
|
|
205
|
-
- Every run, end-of-paragraph and default run property carries `lang` set to the resolved OOXML tag instead of a fixed `en-US`. That tag is
|
|
219
|
+
- Every run, end-of-paragraph and default run property carries `lang` set to the resolved OOXML tag instead of a fixed `en-US`. That tag is core's curated `ooxmlLang` for the language (for example `ja-JP` for `ja` or `ar-SA` for `ar`) or an authored region tag such as `en-NZ`. `altLang` is not written: it names the editing-UI language, which OPF does not model.
|
|
206
220
|
- Theme major/minor `a:ea`/`a:cs` are written only for a slot where a script font is actually selected (FF-49): an explicit `eastAsian`/`complexScript` on the design font scheme, the scheme's own script family, or the language's script font (a language that uses the East Asian slot, such as Japanese, writes `ea`; one that uses the complex-script slot, such as Arabic, Hebrew, Indic or Thai, writes `cs`). Every other slot keeps the vendored empty typeface, exactly as Office's own themes leave it, so PowerPoint picks its per-language default for script text typed later and the package names nothing the author did not select (owner font policy). Latin, Cyrillic, Greek, Armenian, Georgian and Ethiopic decks write neither; the other slot of a CJK or complex-script deck is empty too. The language never changes the theme's latin fonts (Model C): `design.fontScheme` alone sets them. The preview resolves the same slots (opf-render's script profile: the theme's family where it names one, the latin family where it is empty), and `test/theme-script-slots.mjs` checks preview against export for every catalog font scheme.
|
|
207
221
|
- The theme's per-script entry for the language's own script (for example `Jpan`, `Hang`, `Arab` or `Deva`) names the resolver's supplement. The rest of the vendored Office per-script list is unchanged; that list is FF-08's call.
|
|
208
222
|
- Slide and notes runs name no `a:ea`/`a:cs` (FF-05: PowerPoint lists an explicit run East Asian or complex-script typeface as an empty-name font in `Presentation.Fonts`); their script text takes the theme slots through `+mn-ea`/`+mn-cs`. The theme `ea` always names a font (the selected script font, an East Asian font for East Asian text in the deck, else the latin family), so no paragraph end mark reads an empty font. Chart text keeps its own latin/ea/cs in the chart's body font.
|
|
@@ -212,17 +226,21 @@ The exporter reads the presentation `language` through core `resolveScriptFonts(
|
|
|
212
226
|
|
|
213
227
|
Charts keep their `c:lang` and left-to-right label paragraphs. The embedded chart workbook is untouched.
|
|
214
228
|
|
|
215
|
-
When the package carries an FF-32 stored `language` ([document round trip](docs/document-roundtrip.md)), that reference is restored while the runs still carry its OOXML tag (or none); if the runs now use another tag, the observed language below is kept and `metadata-reference-changed` is reported once. Otherwise `fromPptx` sets `language`
|
|
229
|
+
When the package carries an FF-32 stored `language` ([document round trip](docs/document-roundtrip.md)), that reference is restored while the runs still carry its OOXML tag (or none); if the runs now use another tag, the observed language below is kept and `metadata-reference-changed` is reported once. Otherwise `fromPptx` sets `language` to the most common run `lang`, as that BCP-47 tag (OPF 0.15: language is an engine vocabulary, not a catalog): a deck authored as `ja` exports `ja-JP` runs and, without provenance, imports as `ja-JP`; `en-NZ` imports as `en-NZ`. Diagnostics:
|
|
216
230
|
|
|
217
231
|
- `mixed-run-languages`: runs use several tags; the most common one is imported.
|
|
218
|
-
- `language-ambiguous`: records share one curated tag and none has it as its own tag (`bn-BD` is Bengali and Chittagonian, `fil-PH` Filipino and Tagalog); the record of the same primary language is imported.
|
|
219
|
-
- `language-uncatalogued`: no catalog record matches; the tag is imported.
|
|
220
232
|
- `rtl-language-mismatch`: right-to-left paragraphs under a left-to-right language.
|
|
221
233
|
- `script-font-not-imported`: theme `ea`/`cs` name a font that neither repeats latin nor matches what the imported document resolves for the first slide (path `design.fontScheme`), or a slide on another slide master has a master theme whose `ea`/`cs` the imported slide does not resolve to (path `slides.N.design.fontScheme`; with FF-32 provenance the slide's font scheme is restored and nothing is reported). Without provenance, imported OPF does not carry the script slots.
|
|
222
234
|
|
|
223
|
-
Exports report `language-unresolved` when the document's language
|
|
235
|
+
Exports report `language-unresolved` when core cannot resolve the document's language tag (a tag whose script it does not know) and `en-US` is used.
|
|
224
236
|
|
|
225
|
-
|
|
237
|
+
## Small conveniences (FA-13)
|
|
238
|
+
|
|
239
|
+
- **`code.highlight`** exports one native rectangle per run of marked lines (`OPF code N highlight M`) between the code panel and the one-text-box-per-line shapes; marked lines keep the syntax colours on the band and the others are dimmed, both at least 4.5:1 (core `codeHighlightColors`). The bands carry code provenance tags, so they never import as text and a band deleted in PowerPoint loses nothing; import restores `highlight` from the code tag.
|
|
240
|
+
- **Text watermark** (`design.watermark = { text, opacity }`, or a slide's own): one native text box per slide named `OPF watermark text`, first in the shape tree, in the heading font, bold, the theme text color with the opacity as text alpha, centered and rotated 30 degrees counterclockwise (`a:xfrm rot="19800000"`), sized by core `layoutWatermark`. It carries an `OPF_WATERMARK_V1` tag with `kind: "text"`; import restores `design.watermark` from an unchanged box and reports `invalid-watermark-provenance` for an edited one (the text then imports as ordinary text).
|
|
241
|
+
- **`TextRun.code`** writes the design's code family (the font scheme's `code` role, else Roboto Mono) on the run. In a package this exporter wrote, import reads a run in the design's code family back as `code: true`.
|
|
242
|
+
- **`TextRun.lang`** writes `a:rPr lang` (core's curated OOXML tag for the language, such as `fr-FR` for `fr`) with `altLang="en-US"`, plus the run's own East Asian and complex-script faces for that language (`Meiryo` for ja-JP, `Microsoft YaHei` for zh-CN) where the language selects one. A run in the deck language writes no override. Import reads a run `lang` that differs from the deck language back into body text runs.
|
|
243
|
+
- **Presets `1:1`, `4:5`, `9:16`** export as `p:sldSz` 6858000 x 6858000, 6858000 x 8572500 and 6858000 x 12192000 EMU. The exporter's document provenance restores the authored preset on import; a package without it imports the exact size as inches, which resolve to the same canvas.
|
|
226
244
|
|
|
227
245
|
## Numbered lists (RR-33)
|
|
228
246
|
|
|
@@ -236,7 +254,7 @@ A deck that declares content variables, or is marked `"template": true`, is reso
|
|
|
236
254
|
const pptx = await toPptx(template, { variables: { client: 'Globex', revenue: 1250000 } });
|
|
237
255
|
```
|
|
238
256
|
|
|
239
|
-
A template exports with each unfilled variable's `example` and reports `variable-example-used` through `onDiagnostic`; a normal deck with an unfilled required variable throws `OPFPptxError` with code `unfilled-variables`, and a value of the wrong kind throws `invalid-variables`. A template plus values exports the same bytes as the equivalent hand-written deck (`test/template-variables.mjs`). The package stores the resolved deck, not the template form: `fromPptx` returns the filled deck. Decks without content variables are untouched.
|
|
257
|
+
A template exports with each unfilled variable's `example` and reports `variable-example-used` through `onDiagnostic` (a built-in variable such as `{{speaker.name}}` is read from the document's own `speaker`, `organization` and `name`; one with no source value exports as nothing and reports `variable-builtin-missing`); a normal deck with an unfilled required variable throws `OPFPptxError` with code `unfilled-variables`, and a value of the wrong kind throws `invalid-variables`. A template plus values exports the same bytes as the equivalent hand-written deck (`test/template-variables.mjs`). The package stores the resolved deck, not the template form: `fromPptx` returns the filled deck. Decks without content variables are untouched. See [templates and variables](https://github.com/OpenPresentation/opf/blob/main/docs/templates-and-variables.md).
|
|
240
258
|
|
|
241
259
|
## Runtime Policy
|
|
242
260
|
|
|
@@ -277,9 +295,9 @@ The current checkout uses `@openpresentation/opf/composition` for portable geome
|
|
|
277
295
|
|
|
278
296
|
Version 0.4.0 requires published `@openpresentation/opf@^0.6.0`. The optional renderer peer requires `@openpresentation/opf-render@^0.4.0`. Clean registry installs support the new composition APIs without sibling checkouts. For coordinated source development, build OPF and run `node scripts/link-ecosystem.mjs` there; `pnpm test:ecosystem` verifies shared geometry and import/export behavior.
|
|
279
297
|
|
|
280
|
-
For crowded drafts, run `
|
|
298
|
+
For crowded drafts, run `paginate(presentation, { fonts })` from `@openpresentation/opf` first, then pass its returned presentation to both preview and `toPptx`. Native table row sizing now follows shared reference geometry; the exporter does not add hidden table continuation slides.
|
|
281
299
|
|
|
282
|
-
Pass the same `
|
|
300
|
+
Pass the same `fonts` handle used by preview and pagination to `toPptx`. Plain text and headings retain the measured line breaks in editable PowerPoint shapes.
|
|
283
301
|
|
|
284
302
|
The PPTX always names the font family the document chose (for example `typeface="Aptos"`), so PowerPoint opens the file and shows the actual font, installed or as a Microsoft 365 cloud font. This is the [OPF font policy](https://github.com/OpenPresentation/opf/blob/main/docs/font-fidelity.md#font-policy-ff-31): the user's selection is the source of truth, licensed fonts are never bundled or embedded, and previews draw an open look-alike instead. A provider may preview that family with another face: a metric-compatible substitute (Carlito for Calibri, the goal), a visual-only one (Roboto or Carlito for Aptos, a documented fallback and known layout-fidelity gap until a metric-compatible replacement exists), a caller alias or a generic fallback. That face changes measurement and drawing only. It never reaches the theme, runs, bullets or chart parts. Faces of the chosen family itself, such as Roboto Medium for Roboto at weight 500, keep their native style-link names. `test/export-chosen-fonts.mjs` checks this for every renderer Office-pack substitute. The exporter does not embed font binaries. Viewers resolve the named family themselves. When the preview used a non-metric substitute, PowerPoint can break lines differently from the preview.
|
|
285
303
|
|
|
@@ -289,7 +307,7 @@ The exact [pptxgenjs-plus](https://github.com/lofcz/pptxgenjs-plus) 4.3.4 ESM di
|
|
|
289
307
|
|
|
290
308
|
Version 0.4.0 imports and exports supported rich table cells and headers as editable runs, retaining resolved fonts, emphasis, color/alpha, hyperlinks, script positions and explicit line breaks. Import reads native XML, including paragraph defaults and significant whitespace; unstyled body cells remain strings. Shared core 0.6.0 layout grows rows for multiline content and fits text consistently with renderer 0.4.0 without inserting measured soft wraps. Native PowerPoint rendering remains unverified.
|
|
291
309
|
|
|
292
|
-
The exporter measures every cell with the same
|
|
310
|
+
The exporter measures every cell with the same fonts handle, font roles and effective nested `minFontSize` used by the SVG preview. Native table cells retain the original strings and values as text, with matching fitted sizes, line spacing, alignment, margins and row/column geometry. Uneven rows receive empty cells for missing columns. Theme border colors now use the same slot as the preview.
|
|
293
311
|
|
|
294
312
|
`npm test` compares exported OOXML against the published SVG renderer across 168 cells, including 24 cases that require taller rows, Roboto, and Calibri measured with its metric-compatible Carlito substitute (the cells still name Calibri), two canvas sizes, headers and all three alignments. PowerPoint still performs its own natural wrapping and needs the named fonts installed. These document-property checks do not establish native raster parity or lossless typed-cell import.
|
|
295
313
|
|
|
@@ -297,20 +315,22 @@ A local macOS Quick Look check opened both Roboto and system-Arial specimens. Qu
|
|
|
297
315
|
|
|
298
316
|
## Image geometry
|
|
299
317
|
|
|
300
|
-
|
|
318
|
+
Every content picture is an image block (OPF 0.15; `Slide.image` is shorthand for one). Core composes it as `item.image` (`@openpresentation/opf/composition`): the picture frame, the fit (the block's `fit`, else the effective `design.imageFit`, else `cover`), the focus point and the treatment. The export crops, pads and focuses the embedded bytes with core's `fitImage`, so the preview and the export show the same part of the picture: `cover` fills the frame with a native crop (`a:srcRect`) that keeps the `focus` point in view, `contain` shows the whole picture centered, and `stretch` fills the frame exactly. A plain contained picture in the content flow is framed by the picture itself; a placed or treated one keeps core's frame and pads with negative `a:srcRect` insets, so its mask, line and overlay apply to that frame. Slide settings override presentation settings. Geometry is calculated from the exact bytes embedded after asset resolution, so host resolvers are called once. PNG, JPEG, GIF and WebP dimension headers are supported; unsupported or unreadable dimensions produce a path-specific error rather than a distorted picture. Supply supported raster bytes through `imageResolver` for other formats.
|
|
301
319
|
|
|
302
320
|
JPEG EXIF orientations 1–8 are represented by native picture rotation and mirroring. The embedded copy's orientation tag is normalized to 1 to avoid viewer-dependent double rotation. Compressed pixels and other metadata remain unchanged; input data is not mutated. EXIF orientation in other containers, animated playback, SVG/vector assets, effects and lossless crop/orientation import are not covered by this change.
|
|
303
321
|
|
|
304
|
-
|
|
322
|
+
An image block with `placement` (`{edge, size, inset}`) bleeds to that edge: its picture is drawn at the band core composes, in core's item order (after the headings, before the flowed blocks, above the watermark, as the preview draws it), and the headings and the other blocks compose in the rest of the slide. A layout's image placeholder may carry the placement for the slide's image.
|
|
305
323
|
|
|
306
|
-
|
|
324
|
+
Image block treatments export from core's normalized geometry as native DrawingML, the mapping 0.14's slide image used:
|
|
307
325
|
|
|
308
326
|
- `shape` becomes the picture's `a:prstGeom` (`rect`, `roundRect`, `ellipse` or `hexagon`) with core's guide values.
|
|
309
327
|
- `border` becomes a centered solid `a:ln` with a miter join.
|
|
310
328
|
- `recolor` becomes `a:grayscl` or `a:duotone`, followed by `a:alphaModFix` for `opacity`, on the blip only.
|
|
311
|
-
- `overlay` becomes one tagged `OPF
|
|
329
|
+
- `overlay` becomes one tagged `OPF image N overlay` shape directly above the picture, in the frame's shape or as an edge band.
|
|
330
|
+
|
|
331
|
+
An image block with any of its own fields (`fit`, `focus`, a treatment, `placement`) carries an `OPF_IMAGE_V1` shape tag with those fields and the exact native picture geometry; its overlay carries `OPF_IMAGE_OVERLAY_V1`. On import an unchanged tagged picture becomes the same image block again, with the embedded bytes as its data URI source. An edited, duplicated or ambiguous tagged picture is imported as an ordinary image block and reports `invalid-image-provenance`; an edited overlay drops only the overlay. Native PowerPoint raster parity for negative `a:srcRect` insets has not been checked with Office yet.
|
|
312
332
|
|
|
313
|
-
A deck or slide watermark (`design.watermark`) exports as one native picture named `OPF watermark` per slide, after the
|
|
333
|
+
A deck or slide watermark (`design.watermark`) exports as one native picture named `OPF watermark` per slide, after the image background and its overlay, if any, and before all content (placed image blocks included) (the preview's paint order, so a background picture lies beneath the watermark), at the preview's frame: the centered 40% by 40% box at 30%/30% of the slide, fitted without cropping. Its opacity is `a:alphaModFix` (the object form's `opacity`, otherwise the preview default 0.08), and its alt text is the asset's `alt`, otherwise `Watermark`. `design.watermark = false` on a slide exports none. An unchanged export imports back as `design.watermark` (at deck level when every slide carries the same one); an edited picture stays ordinary content and reports `invalid-watermark-provenance`. An unresolved image reports `unresolved-asset`; `src` is required by the schema, so a watermark with no image source is a validation error and not an export diagnostic.
|
|
314
334
|
|
|
315
335
|
### Brand assets, picture bullets and the accent font (spec-gap closure A)
|
|
316
336
|
|
|
@@ -319,9 +339,9 @@ These design fields need the core that composes them (`resolveLogo`, `geometry.l
|
|
|
319
339
|
- **Cover and section logo** (`design.logo`, a slide's own `design.logo`, else the primary organization's `logo`). One native picture named `OPF logo`, after the watermark and before the content, fitted without cropping into core's `geometry.logo` box (56 px high at 720 px, up to four times as wide), anchored at the box's left edge and vertically centered, exactly the preview's `xMinYMid meet`. Its alt text is the asset's `alt`, otherwise `Logo`. A LogoSet picks its variant from the slide background (a dark background takes the light variants); the exporter's dark test is the one that already chooses the text colour. An unresolved or unreadable logo draws the preview's "Image unavailable" panel in the box and reports `unresolved-asset` at the logo's path (`strictAssets` throws). Content slides never get one. The picture carries an `OPF_LOGO_V1` tag (slide, source path, variant, the exact picture properties): an unchanged export consumes the picture on import (it is not content; `design.logo` itself returns from the document tag), and an edited, ambiguous or damaged one stays an ordinary picture and reports `invalid-logo-provenance`. When nothing else restored a logo (an export with `provenance: false`, or with `references-only`, which stores no sources), the consumed picture's own image becomes `design.logo` (the slide's own for a slide-level logo), so the logo is never lost; it is a single image, not the LogoSet. A PPTX with no OPF tags imports the picture as an ordinary image block and invents no `design.logo`.
|
|
320
340
|
- **Header and footer `logo: true`.** A generated image part in the part's box (the icon variant), exported like an `image` part with the furniture picture tag replaced by an `OPF_LOGO_V1` tag with role `furniture`. The slide's furniture manifest lists it under a separate `logos` key (`{kind, zone, drawn}`), never in `parts` or `definitions`, because every released importer validates those two strictly. It re-imports as `logo: true`, never as an `image` data URI; without a resolvable logo core reports `unresolved-content` at `<zone>.logo`, nothing is drawn and `logo: true` still returns (`drawn: false`).
|
|
321
341
|
- **Picture bullets** (`design.listBullet: image`, resolved to the icon logo by core as `item.bulletImage`). Every entry's first line carries `<a:buSzPct val="100000"/><a:buBlip><a:blip r:embed=…/></a:buBlip>` in place of the character bullet, so the marker is the text size, like the preview's square marker. PptxGenJS embeds the icon once per slide (a picture named `OPF bullet image`); packaging removes that picture and keeps the media part and its relationship for the `a:buBlip` elements. Marker indent and text positions are those of the character bullets. An icon that cannot be embedded keeps the character bullets and reports `unresolved-asset` once per slide at the logo's path. Import recognizes `a:buBlip` paragraphs as list paragraphs; `design.listBullet` returns from the stored design hint (a PPTX without tags has no hint). PowerPoint shows a picture bullet at the picture's own aspect ratio; use a square icon for the preview's square marker. The released 0.11.6 importer does not know `a:buBlip` and imports such a list as plain text lines.
|
|
322
|
-
- **`fontScheme.accent`.** Core resolves the accent role for the slide tag (eyebrow) and the quote body, so their runs carry the accent typeface in `a:latin`, `a:ea` and `a:cs` with the catalog's pitch family when the family is known; theme major/minor fonts, every other run and the theme are unchanged. The accent family appears in `docProps/app.xml` "Fonts Used" like any other used font; `
|
|
342
|
+
- **`fontScheme.accent`.** Core resolves the accent role for the slide tag (eyebrow) and the quote body, so their runs carry the accent typeface in `a:latin`, `a:ea` and `a:cs` with the catalog's pitch family when the family is known; theme major/minor fonts, every other run and the theme are unchanged. The accent family appears in `docProps/app.xml` "Fonts Used" like any other used font; `checkTypefaces` callers list it in `options.fonts`. `design.fontScheme` (with `accent`) returns from the document tag.
|
|
323
343
|
|
|
324
|
-
An unchanged export imports every treatment field back. An edited overlay drops only the overlay and reports `invalid-
|
|
344
|
+
An unchanged export imports every treatment field back. An edited overlay drops only the overlay and reports `invalid-image-provenance`. An edited picture or effect drops the treatment: the picture stays an ordinary image block. See core `docs/image-treatments.md` for the vocabulary and the unsupported effects: blur, shadows, soft edges and background removal. PowerPoint's luminance weights for grayscale and duotone are unverified natively.
|
|
325
345
|
|
|
326
346
|
Tests compare SVG/native fit and crop geometry across nine synthetic raster fixtures and cover all eight JPEG orientations. Keynote 14.4 visually preserves proportions for wide/tall fit/crop and displays all eight orientations correctly. This does not establish Microsoft PowerPoint raster parity or WebP support in every Office version.
|
|
327
347
|
|
|
@@ -358,14 +378,14 @@ Pattern backgrounds export as a native `<a:pattFill>` with explicit foreground a
|
|
|
358
378
|
|
|
359
379
|
`code.language` and `metric.trend` export natively from the same core tables the preview reads (RR-07). Code keeps one text box per source line; a language adds coloured runs (theme-derived colours kept at 4.5:1 or more on the #111827 panel) whose text concatenates to the exact line, and an unknown language is exported plain. A trend adds one arrow shape (`upArrow`, `downArrow` or `rightArrow`, alt text "Trend: up") beside the trend word and colours the delta and trend text green, red or neutral; the arrow is part of the metric's provenance group, so the metric re-imports as the one authored payload. `scripts/rr07-native-set.mjs <dir>` builds the native-check decks, preview PNGs and a manifest.
|
|
360
380
|
|
|
361
|
-
|
|
381
|
+
An image background (`{type: "image", src, alt, fit, focus, opacity, recolor, overlay}`, or an image source string, a cover image; core composes it as `backgroundImage` from the slide, deck or theme background) never moves content. Without alt text it exports as the native slide background, an `<a:blipFill>` for the embedded raster (`data:` sources, declared `asset:` ids and `imageResolver` results): `cover` (the default) crops the picture to the slide with `a:srcRect` around the `focus` point (core's `fitImage`), `contain` centers the whole picture with `a:fillRect` insets, `stretch` fills the slide, and `tile` repeats the picture at its intrinsic size (1 picture pixel = 1 reference pixel, 1/96 in) from the top-left corner. With `dpi="0"`, a native tile is sized from the raster's own resolution: PNG `pHYs` in metres; JPEG JFIF density in inches or centimetres, otherwise EXIF `XResolution`/`YResolution`; 96 dpi when absent. The tile scale is that resolution over 96 on each axis. `recolor` becomes `a:grayscl` or `a:duotone` and opacity `a:alphaModFix` on the blip, as for image blocks. With alt text (the background's own only: a referenced asset's alt does not make a background meaningful) the picture is a full-slide `p:pic` at the back of the slide, named `OPF background slides.N`, carrying the text as its `descr` so screen readers read it; the slide background is then the colour scheme's default background colour, which shows around a contained picture and through a translucent one. The overlay is one shape directly above the picture (`OPF background overlay slides.N` for a native fill, `OPF background slides.N overlay` for the picture): the whole slide, or a band along one edge. WebP sources become PNG parts as for pictures. An unresolved image keeps the background color and reports `unresolved-asset` at the background's path (`strictAssets` throws). A JPEG EXIF orientation cannot rotate a background fill, so it reports `unsupported-background-image-orientation`.
|
|
362
382
|
|
|
363
|
-
Import reads `a:pattFill` with a DrawingML preset and resolvable foreground/background colors, including slide/layout/master inheritance and theme references, as an OPF pattern background. Import does not guess undefined default colors. Import also reads embedded `a:blipFill` pictures as an image background with the exact image bytes as a `data:` URI. Tiles become `tile`. If the tile scale, offset, alignment, flip or source insets differ from the geometry OPF `tile` exports (
|
|
383
|
+
Import reads `a:pattFill` with a DrawingML preset and resolvable foreground/background colors, including slide/layout/master inheritance and theme references, as an OPF pattern background. Import does not guess undefined default colors. Import also reads embedded `a:blipFill` pictures as an image background with the exact image bytes as a `data:` URI. Tiles become `tile`. If the tile scale, offset, alignment, flip or source insets differ from the geometry OPF `tile` exports (the picture at its intrinsic size), the import reports `approximate-background-image`. A crop matching the slide aspect becomes `cover`, with the `focus` the crop centers (a crop pinned to an edge reads as the edge-most focus with that crop); centered fill insets matching the image aspect become `contain`; an uncropped fill of another aspect becomes `stretch`. Padded, offset or effect-bearing fills become the closest fit and report `approximate-background-image`. `a:grayscl` and an `a:duotone` of two literal colours become `recolor`. An unchanged tagged background picture (`OPF_IMAGE_V1`) becomes the image background with its alt text, fit, focus, opacity and recolor, and an unchanged tagged overlay (`OPF_IMAGE_OVERLAY_V1`) its overlay. Linked or unreadable pictures report `unsupported-background-image`. `test/background-fills.mjs` and `test/background-images.mjs` cover export, import, native edits, inheritance and diagnostics. Microsoft PowerPoint rendering of these fills has not been verified natively: the resolution-based tile scale, a tiled `p:pic` fill, what PowerPoint shows around a contained or through a translucent native background fill (it has nothing behind the slide background), and negative `a:srcRect` insets.
|
|
364
384
|
|
|
365
385
|
|
|
366
386
|
## Theme color scheme
|
|
367
387
|
|
|
368
|
-
Export writes the deck's resolved color scheme into `ppt/theme/theme1.xml` `a:clrScheme`, named after the scheme. The OPF slots map one to one: `dark1`/`light1`/`dark2`/`light2` to `dk1`/`lt1`/`dk2`/`lt2`, `accent1`-`accent6` to themselves, and `hyperlink`/`followedHyperlink` to `hlink`/`folHlink`. An abstract role fills a slot only when the scheme leaves that slot unset. Previously every export carried the vendored Office palette (`accent1` `4472C4`). When the deck
|
|
388
|
+
Export writes the deck's resolved color scheme into `ppt/theme/theme1.xml` `a:clrScheme`, named after the scheme. The OPF slots map one to one: `dark1`/`light1`/`dark2`/`light2` to `dk1`/`lt1`/`dk2`/`lt2`, `accent1`-`accent6` to themselves, and `hyperlink`/`followedHyperlink` to `hlink`/`folHlink`. An abstract role fills a slot only when the scheme leaves that slot unset. Previously every export carried the vendored Office palette (`accent1` `4472C4`). When the deck's `design.theme` resolves (embedded, or in a registered catalog), `a:theme` and its `thm15:themeFamily` take that theme's name (else its id). A deck with no resolvable colour scheme writes core's engine default slots under the name `OpenPresentation`. The rewrite happens during package normalization; the vendored PptxGenJS bytes are unchanged.
|
|
369
389
|
|
|
370
390
|
Document colors that name a slot or role (`accent2`, `textSecondary`, `surface`, ...) become `a:schemeClr` in runs, table cell fills and text, and table borders. Theme-slot backgrounds (`{type:'theme', slot}` or a slot name) do the same. Slide content reaches the theme through the master color map, so `dark1`, `light1`, `dark2` and `light2` are written as `tx1`, `bg1`, `tx2` and `bg2`. A reference becomes `schemeClr` only when the deck theme slot holds exactly the color the slide resolved. PowerPoint has one theme per master, so a slide with its own `design.colorScheme` (or a slide theme that changes any slot) keeps **every** color literal, including values that happen to equal the deck theme, so a theme edit never partially recolors it.
|
|
371
391
|
|
|
@@ -382,7 +402,7 @@ These stay `a:srgbClr`:
|
|
|
382
402
|
- translucent colors;
|
|
383
403
|
- other engine-derived chrome: card fills and borders, chart panels, series and labels, timeline connectors and markers, and image and media placeholders, plus default text on a card or a literal background. These colors are contrast-selected or derived from roles, not named by the document, and are not yet theme references.
|
|
384
404
|
|
|
385
|
-
Import reads the first slide master's theme. An exact twelve-slot match with
|
|
405
|
+
Import reads the first slide master's theme and matches it against the host default catalog only (the first of `fromPptx`'s `catalogs`; nothing is bundled): the bare ids it imports resolve in a document that declares no catalogs. An exact twelve-slot match with one of its colour schemes returns that id; the `clrScheme` name breaks ties. Otherwise the importer returns inline slots, relative to the scheme the `clrScheme` is named after when there is one (`{id:'boost', accent1:'#123456'}`). A theme whose name equals one of its themes' names maps to `design.theme` only if the package's color scheme or heading/body fonts match that theme (its references resolved in its own group first); otherwise `theme-unverified` is reported. With no catalog registered, or a match only in another registered catalog, the colour scheme imports inline and no theme id is recovered; an FF-32 stored reference still restores. A missing theme, or slots that are not opaque sRGB/system colors, report `unsupported-theme-colors` on `design.colorScheme`. Slide colors are still imported as resolved hex. Role overrides such as `primary` have no theme slot and are not recovered. `test/theme-colors.mjs` covers all 14 gallery schemes and 4 gallery themes (registered as `defaultCatalog`), import without catalogs, override, foreign and damaged themes, and the literal-color boundaries. `test/table-theme-colors.mjs` covers named and default table chrome on all 14 schemes and four backgrounds, literals, override slides, the link colors, re-import and a theme switch in the package. This establishes package structure and round-trip, not PowerPoint rendering.
|
|
386
406
|
|
|
387
407
|
## JPEG orientation on import
|
|
388
408
|
|
|
@@ -37,7 +37,7 @@ export function addFitLines(slide, value, fit, box, context, helpers, {objectNam
|
|
|
37
37
|
return {text: fragment.text, options: {...helpers.nativeFontOptions(fragment.style), fontSize: (fragment.nominalSize ?? fragment.fontSize) * .75, color: native,
|
|
38
38
|
underline: fragment.run.underline ? {style: 'sng', color: native} : undefined, strike: fragment.run.strikethrough ? 'sngStrike' : undefined,
|
|
39
39
|
baseline: fragment.baselineShift ? -fragment.baselineShift / (fragment.nominalSize ?? fragment.fontSize) * 2000 : undefined,
|
|
40
|
-
hyperlink: fragment.kind !== 'marker' && fragment.run.link && /^(https?:|mailto:)/i.test(fragment.run.link) ? {url: fragment.run.link} : undefined}};
|
|
40
|
+
hyperlink: fragment.kind !== 'marker' && fragment.run.link && /^(https?:|mailto:|tel:)/i.test(fragment.run.link) ? {url: fragment.run.link} : undefined}};
|
|
41
41
|
});
|
|
42
42
|
if (!runs.length) runs.push({text: '', options: {}});
|
|
43
43
|
const placed = fit.placement?.lines[index];
|
|
@@ -176,6 +176,8 @@ function runArrays(slide) {
|
|
|
176
176
|
if (!object(host)) return;
|
|
177
177
|
if (Array.isArray(host.blocks)) { host.blocks.forEach((block, index) => visit(block, `${path}.blocks.${index}`)); return; }
|
|
178
178
|
if (Array.isArray(host.text)) found.push({path: `${path}.text`, get: () => host.text, set: value => { host.text = value; }});
|
|
179
|
+
// FA-10: a quote's text may carry markers too.
|
|
180
|
+
if (object(host.quote) && Array.isArray(host.quote.text)) found.push({path: `${path}.quote.text`, get: () => host.quote.text, set: value => { host.quote.text = value; }});
|
|
179
181
|
for (const field of ['bullets', 'items']) if (Array.isArray(host[field])) host[field].forEach((item, index) => {
|
|
180
182
|
if (Array.isArray(item)) found.push({path: `${path}.${field}.${index}`, get: () => host[field][index], set: value => { host[field][index] = value; }});
|
|
181
183
|
else if (object(item)) {
|
|
@@ -184,6 +186,8 @@ function runArrays(slide) {
|
|
|
184
186
|
}
|
|
185
187
|
});
|
|
186
188
|
};
|
|
189
|
+
// FA-10: the heading group (tag, title, subtitle) may carry markers when it is TextRun[].
|
|
190
|
+
for (const field of ['tag', 'title', 'subtitle']) if (Array.isArray(slide[field])) found.push({path: field, get: () => slide[field], set: value => { slide[field] = value; }});
|
|
187
191
|
const keys = Object.keys(slide).filter(key => /^(top|middle|bottom|left|center|right)([+:]|$)/.test(key) && object(slide[key]));
|
|
188
192
|
if (keys.length) for (const key of keys.sort()) visit(slide[key], key); else visit(slide, '');
|
|
189
193
|
return found;
|
|
@@ -97,14 +97,17 @@ export function importBackground(slidePath, dimensions, archive, report) {
|
|
|
97
97
|
return readNativeBackground(drawingObject([style]), dimensions, report, context);
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
-
const imageEffects = new Set(['r:embed', 'r:link', 'cstate', 'a:alphaModFix', 'a:lum', 'a:extLst']);
|
|
100
|
+
const imageEffects = new Set(['r:embed', 'r:link', 'cstate', 'a:alphaModFix', 'a:grayscl', 'a:duotone', 'a:lum', 'a:extLst']);
|
|
101
101
|
const inset = (rect, key) => Number(rect?.[key] ?? 0) / 100000;
|
|
102
102
|
const near = (a, b) => Math.abs(a - b) <= .005;
|
|
103
|
+
const focusValue = value => Math.round(Math.max(0, Math.min(1, value)) * 10000) / 10000;
|
|
103
104
|
|
|
104
|
-
// Import an embedded raster picture fill as an OPF image background
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
105
|
+
// Import an embedded raster picture fill as an OPF image background ({type: 'image', src, fit, focus, opacity}), the
|
|
106
|
+
// inverse of nativeImageBackgroundFill (FA-23): an intrinsic-size top-left tile is `tile`; a crop that keeps the slide's
|
|
107
|
+
// aspect is `cover`, with the focus point the crop centers (a crop pinned to an edge cannot say how far past the edge
|
|
108
|
+
// the focus was, so it reads as the edge-most focus that gives the same crop); centered fill-rectangle insets that keep
|
|
109
|
+
// the picture's aspect are `contain`; an uncropped fill of another aspect is `stretch`. Any other geometry or picture
|
|
110
|
+
// effect is imported as the closest fit and reported.
|
|
108
111
|
function readImageBackground(fill, partPath, {relationships, bytes}, dimensions, report) {
|
|
109
112
|
const blip = fill['a:blip'] ?? {};
|
|
110
113
|
const relationship = blip['r:embed'] && relationships(partPath).get(blip['r:embed']);
|
|
@@ -116,23 +119,31 @@ function readImageBackground(fill, partPath, {relationships, bytes}, dimensions,
|
|
|
116
119
|
}
|
|
117
120
|
const approximate = reason => report({code: 'approximate-background-image', message: `The native background picture ${reason}; it was imported as the closest OPF image background.`});
|
|
118
121
|
const lum = blip['a:lum'];
|
|
119
|
-
if (Object.keys(blip).some(key => !imageEffects.has(key)) || (lum && Object.keys(lum).length)) approximate('uses picture effects outside OPF opacity');
|
|
122
|
+
if (Object.keys(blip).some(key => !imageEffects.has(key)) || (lum && Object.keys(lum).length)) approximate('uses picture effects outside OPF opacity and recolor');
|
|
123
|
+
// a:grayscl and an a:duotone of two literal colours are the OPF recolor (FA-22 draft 3).
|
|
124
|
+
let recolor;
|
|
125
|
+
if (blip['a:grayscl'] !== undefined) recolor = 'grayscale';
|
|
126
|
+
const duotone = blip['a:duotone'], literal = node => /^[\da-f]{6}$/i.test(node?.val ?? '') && Object.keys(node).every(key => key === 'val' || typeof key === 'symbol') ? `#${node.val.toUpperCase()}` : undefined;
|
|
127
|
+
if (duotone) {
|
|
128
|
+
const colors = Array.isArray(duotone['a:srgbClr']) ? duotone['a:srgbClr'] : [];
|
|
129
|
+
const [dark, light] = colors.map(literal);
|
|
130
|
+
if (colors.length === 2 && dark && light && Object.keys(duotone).every(key => key === 'a:srgbClr' || typeof key === 'symbol')) recolor = {dark, light};
|
|
131
|
+
else approximate('uses a duotone recolor that is not two literal colours');
|
|
132
|
+
}
|
|
133
|
+
if (recolor && blip['a:grayscl'] !== undefined && duotone) approximate('uses both a grayscale and a duotone recolor');
|
|
120
134
|
const amount = blip['a:alphaModFix'] ? Number(blip['a:alphaModFix'].amt ?? 100000) / 100000 : 1;
|
|
121
135
|
const opacity = Number.isFinite(amount) ? Math.max(0, Math.min(1, amount)) : 1;
|
|
122
|
-
let fit = 'cover';
|
|
136
|
+
let fit = 'cover', focus;
|
|
123
137
|
if (fill['a:tile']) {
|
|
124
138
|
fit = 'tile';
|
|
125
|
-
// OPF tile has one geometry: top-left min(w,h)/4 cells holding the whole
|
|
126
|
-
// image (the default contain imageFill), at the raster's own resolution.
|
|
127
139
|
const tile = fill['a:tile'], crop = fill['a:srcRect'];
|
|
128
|
-
const expected = nativeTileScale(metadata
|
|
129
|
-
const x = (1 - expected.cell / (metadata.width * expected.scale)) / 2, y = (1 - expected.cell / (metadata.height * expected.scale)) / 2;
|
|
140
|
+
const expected = nativeTileScale(metadata);
|
|
130
141
|
const value = (raw, fallback) => raw === undefined ? fallback : Number(raw);
|
|
131
142
|
const scaled = (raw, target) => Math.abs(value(raw, 100000) - target) <= Math.max(2, target * .005);
|
|
132
143
|
const matches = scaled(tile.sx, expected.sx) && scaled(tile.sy, expected.sy)
|
|
133
144
|
&& Object.entries(nativeTileAlignment).every(([key, native]) => String(tile[key] ?? native) === native)
|
|
134
|
-
&&
|
|
135
|
-
if (!matches) approximate('tiles with a scale, offset, alignment, flip or crop that the OPF tile fit does not express');
|
|
145
|
+
&& ['l', 't', 'r', 'b'].every(key => near(inset(crop, key), 0));
|
|
146
|
+
if (!matches) approximate('tiles with a scale, offset, alignment, flip or crop that the OPF tile fit (intrinsic size from the top-left corner) does not express');
|
|
136
147
|
} else {
|
|
137
148
|
const crop = fill['a:srcRect'], box = fill['a:stretch']?.['a:fillRect'];
|
|
138
149
|
const [l, t, r, b] = ['l', 't', 'r', 'b'].map(key => inset(crop, key));
|
|
@@ -141,10 +152,15 @@ function readImageBackground(fill, partPath, {relationships, bytes}, dimensions,
|
|
|
141
152
|
const target = dimensions.width * (1 - fl - fr) / (dimensions.height * (1 - ft - fb));
|
|
142
153
|
const aspect = Number.isFinite(source) && Number.isFinite(target) && source > 0 && target > 0 && Math.abs(source / target - 1) <= .005;
|
|
143
154
|
const noCrop = [l, t, r, b].every(value => near(value, 0)), noInset = [fl, ft, fr, fb].every(value => near(value, 0));
|
|
144
|
-
if (
|
|
145
|
-
else if (aspect &&
|
|
146
|
-
|
|
155
|
+
if (noCrop && noInset && !aspect) fit = 'stretch';
|
|
156
|
+
else if (aspect && noInset && Math.min(l, t, r, b) >= -.00002) {
|
|
157
|
+
fit = 'cover';
|
|
158
|
+
// fitImage centers the focus point: visible share v = 1 - l - r, focus x = l + v / 2.
|
|
159
|
+
const x = focusValue(l + (1 - l - r) / 2), y = focusValue(t + (1 - t - b) / 2);
|
|
160
|
+
if (!near(x, .5) || !near(y, .5)) focus = {x, y};
|
|
161
|
+
} else if (aspect && noCrop && near(fl, fr) && near(ft, fb) && Math.min(fl, ft) >= 0) fit = 'contain';
|
|
162
|
+
else approximate('is cropped with padding, distorted or offset');
|
|
147
163
|
}
|
|
148
164
|
const binary = typeof Buffer !== 'undefined' ? Buffer.from(data).toString('base64') : btoa(Array.from(data, byte => String.fromCharCode(byte)).join(''));
|
|
149
|
-
return {type: 'image',
|
|
165
|
+
return {type: 'image', src: `data:${metadata.mediaType};base64,${binary}`, ...(fit === 'cover' ? {} : {fit}), ...(focus ? {focus} : {}), ...(opacity === 1 ? {} : {opacity}), ...(recolor ? {recolor} : {})};
|
|
150
166
|
}
|