Skip to content

Repository files navigation

OPF PPTX

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) 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-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-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).

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.

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.

Unfinished prepared shaping work is preserved in the September 15 roadmap; it is not part of the published runtime.

Version 0.8.0 and this checkout require Node 24 (24.x). Use .nvmrc for local development. Earlier published versions retain their original engine declarations. Browser entrypoints remain browser-safe; native application compatibility is verified separately.

Version 0.8.0 consumes shared heading/scalar/rich line placement, including textRasterPadding, through editable native line boxes without autofit. Complete heading tags retain title/subtitle/tag roles, line ordering and current edited text during import. Source boundary metadata also records hard-line separators, so complete scalar/heading groups recover current native text with spaces, tabs and exact authored line endings. Incomplete, duplicate, ambiguous or bulleted groups fall back to ordinary import with diagnostics. Legacy heading tags retain their prior native-line behavior; arbitrary formatting, nesting and geometry are not reconstructed. See the source contract and limits. Native raster certification remains separate.

Pure local PowerPoint conversion tooling for Open Presentation Format documents. This repo owns the Phase 3 and Phase 4 toolkit lanes: OPF to PPTX export and PPTX to OPF import.

Version 0.7.0's shared-code integration preserves exact source/metadata through guarded native tags and editable lines. XML-forbidden code characters reject export with invalid-code-text, the OPF field path and UTF-16 offset; schema validation alone does not establish XML representability. Native source recovery does not reconstruct formatting, geometry or font theme.

Version 0.8.0 includes shared metric integration. Native alignment and exact tested field recovery are implemented; tab-position and raster fidelity gates remain open.

The native text verifier has completed 24 separately bounded cases on each supported Node runtime against its recorded source baseline. All 72 imports and 96 original-text ink masks per runtime pass; parent-owned temporary fonts are removed after success, failure and timeout controls. Those historical results do not cover subsequent font-selection changes, edited-text reflow, arbitrary native fidelity or the unresolved chart and metric gates.

Version 0.8.0 physical-font selection consumes optional fontFace metadata on accepted text styles. The provider supplies the physical family and its boolean bold/italic style-link flags independently of logical numeric weight. This lets a resolved Roboto SemiBold or ExtraBold face keep its regular legacy-family style rather than requesting a second bold style. Providers without metadata retain the previous numeric-weight behavior; malformed metadata fails with invalid-font-selection. Use core 0.10.0 and renderer 0.8.0. Verification and limits distinguish native selection flags from actual Office paint.

Version 0.8.0 also measures design.contentBox cards through core's padded interior and exports editable rounded frames at their outer allocation. Native tags identify unchanged empty generated frames on reimport, with a content-card-reflow diagnostic: frame appearance and placement are not reconstructed. A name alone never hides a shape; edited, untagged or ambiguous frames use ordinary native import, retaining text or an unsupported-shape description. These source checks do not establish native PowerPoint raster fidelity.

Scope

Version 0.8.0 requires core 0.10.0 and uses renderer 0.8.0 for coordinated preview/font measurement. Quotes and code export their accepted internal lines and styles without another fitting pass. Controlled Windows PowerPoint quote evidence records glyph containment, separation, save/reopen and text reimport against its exact source/font hashes. Native quote import restores the quote payload from an unchanged export (see Quote provenance); it does not restore the original typography or readability policy, and an edited or damaged quote imports as editable text blocks. Native chart geometry and general scalar-text wrapping also remain different from preview; editability and valid reimport do not establish raster equivalence.

  • Package: @openpresentation/opf-pptx
  • Repository: OpenPresentation/opf-pptx
  • License: MIT
  • Compatibility target: @openpresentation/opf
  • Renderer relationship: may use @openpresentation/opf-render for chart rasterization, the PNG fallback of SVG pictures and visual verification
  • Public export API: toPptx(opf, opts)
  • Public import API: fromPptx(buffer, opts) (opts.signals adds raw per-shape signals; see Import signals)

The export path validates OPF with @openpresentation/opf, maps slide titles and common content payloads to editable PowerPoint objects through pptxgenjs, then normalizes the generated ZIP for stable entry ordering, fixed timestamps, and reproducible bytes.

import { toPptx } from "@openpresentation/opf-pptx";

const bytes = await toPptx({
  $schema: "https://openpresentation.org/schema/opf/v1",
  name: "Quarterly Review",
  slides: [
    {
      title: "Revenue grew across all regions",
      items: ["North America +18%", "EMEA +14%", "APAC +11%"]
    }
  ]
});

await fs.promises.writeFile("quarterly-review.pptx", bytes);

toPptx returns a Uint8Array containing a PowerPoint-openable .pptx. It does not fetch remote assets. Data URI images and local paths can be embedded directly; hosts that need private asset loading should pass imageResolver(src, context). Set strictAssets: true to turn unresolved or remote image assets into structured OPFPptxError failures instead of editable placeholder boxes.

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.

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 content images, the slide image (design.slideImage), the watermark, header and footer images and logos, and cover and section logos. The picture takes the same frame and fit (contain, design.imageFill: "crop") 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 (a background picture fill carries no SVG), and a slide image 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.

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.

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.

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, design.slideImage) round-trip as SVG.

fromPptx parses an existing .pptx buffer locally and returns an OPF document that validates with @openpresentation/opf:

import { fromPptx, toPptx } from "@openpresentation/opf-pptx";

const opf = await fromPptx(await fs.promises.readFile("source.pptx"));
const roundTripBytes = await toPptx(opf);

await fs.promises.writeFile("round-trip.pptx", roundTripBytes);

The importer reads core properties, slide order, text boxes, speaker notes, embedded images, tables, and basic cached chart data from the OOXML parts. Slides or objects that do not map cleanly fall back to editable blocks[] payloads; OOXML positions are used for deterministic ordering and title/subtitle detection while keeping the emitted OPF schema-valid.

New in 0.10.0: native notes and property whitespace

Current source preserves spaces, tabs, NBSP and authored CR/LF/CRLF in speaker notes and scalar presentation name, description and author; published npm 0.9.1 does not contain this repair (0.10.0 does). Import reads current native notes body paragraphs in run/field/line-break order, retaining blank paragraphs. Explicit paragraph and line-break boundaries import as LF. XML character references decode once, so literal text such as &#13; remains literal. Export writes authored CR as character references in native text, without adding source-recovery tags; ordinary exports with no authored CR remain byte-identical.

Native edits, cleared/deleted note bodies or parts, core-property edits/deletions and slide relationship order remain authoritative in all provenance modes. Empty notes and absent notes still both import absent; an empty or absent title uses the existing fallback, and empty description/author import absent. Author arrays still export as a joined scalar. External literal XML line endings follow XML normalization; exact CR requires character references. These are portable XML conversion controls, not native Office or visual acceptance.

v1 Placeholder and OOXML Mapping

The first exporter keeps the public API stable while using pptxgenjs internally:

  • Slide.title, Slide.subtitle, and Slide.tag become editable text boxes, not PowerPoint master placeholders.
  • 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).
  • 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). 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); 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.
  • 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 ("12%", "$5" and "1,234" count) 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.
  • 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.
  • 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.
  • ZIP entries, generated chart/workbook part names, core-property timestamps, and nested chart workbook timestamps are normalized for reproducible bytes. Export determinism lists the tested time zone, locale, clock and host-font controls and each known variance (WebP conversion, font registries, ICU segmentation, timestamps).
  • 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).
  • checkPptxTypefaces(bytes, {fonts, monospace, themeScripts}) (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) inventories every typeface, workbook font name and "Fonts Used" entry in every XML part, including nested packages, and reports each font outside that policy. inventoryPptxTypefaces() returns the raw inventory.

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.

New in 0.10.0: explicit ZIP dates

The following behavior is in current source; the published npm 0.9.1 package does not contain this repair or tightened option contract; 0.10.0 does.

Omitting zipDate (or passing undefined) keeps the established fixed 1980 ZIP bytes. Explicit zipDate values now encode UTC calendar fields in both the PPTX and embedded workbook ZIPs, independently of the host timezone. This option changes ZIP metadata; the separate timestamp option controls core-property XML.

Accepted values are a valid Date, finite epoch milliseconds, YYYY-MM-DD (UTC midnight), or YYYY-MM-DDTHH:mm[:ss[.fraction]] ending in Z or ±HH:mm. Calendar components must be valid and the resulting UTC year must be 1980–2099, matching the existing ZIP writer's supported range. ZIP timestamps have two-second resolution: fractional and odd seconds are truncated.

This intentionally tightens the previous host-dependent Date parsing contract. Datetimes without a timezone, legacy date strings, invalid dates, and values outside that range throw OPFPptxError with code invalid-zip-date and path options.zipDate. Explicit null, '' and 0 no longer silently use the default (0 is a 1970 epoch date). Existing successful UTC output and default output remain byte-identical; explicit dates on other timezones change to the canonical UTC result.

v1 Import Mapping

Quote provenance (FF-57)

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 (attribution - source). 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 the separator falls, 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.

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.

New in 0.10.0: current native body formatting

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.

This does not reconstruct original source run identities or boundaries between native shapes, infer master/layout text styles, or recover cached authored content. Native paragraph/break boundaries become LF; character-reference CR remains explicit. Unsupported properties report body-path diagnostics rather than claim exact formatting. Reading order still uses current native positions. Published npm 0.9.1 does not contain this representation change; portable conversion checks do not establish Office rendering or general rich-text round-trip fidelity.

The first importer is mechanical and schema-compatible:

  • Presentation core properties map to OPF name, description, and author.
  • The first slide master's theme clrScheme maps to design.colorScheme: a catalog id on an exact twelve-slot match, otherwise inline slots. A theme named like a catalog theme maps to design.theme when its colors or heading/body fonts corroborate it (see Theme color scheme).
  • 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.
  • Remaining text boxes map to blocks[] as text or list payloads, sorted by OOXML position.
  • 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-3x, a filled radar as filled-radar; a scatter chart imports as [Point, X, ...series]).
  • 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.
  • 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.
  • 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.
  • Run lang maps back to the presentation language (a catalog id when it round-trips, else the tag); see Languages, right-to-left text and script fonts.
  • 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.
  • 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.
  • 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.
  • Unknown non-text shapes and unsupported graphic frames become editable text fallback blocks instead of failing the import.
  • 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 id, type, composition, composition hints and the inline layout record) 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). See document round trips for exactly what is embedded.
  • 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 ids 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.
  • 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/).

There is no AI classification pass in the OSS runtime. Hosts can run optional cleanup or semantic remapping after fromPptx returns. A host that wants to rebuild structure (code, metrics, quotes, timelines, regions) in a deck that did not come from OPF can ask for the raw shape facts such a step needs; see Import signals.

Import signals

A deck exported by this package carries OPF provenance tags, so its structure returns exactly. A deck from anywhere else does not: a code panel is a text box with a monospace font, a metric is a large number over a small label, a quote is italic text over an attribution, and fromPptx imports each as plain text, because telling them apart takes judgement. That judgement is not made here. fromPptx only reports what is in the package, so a host can run its own classifier (a model, a rules engine, a person) over it. The runtime policy is unchanged: no network, no models, no clock, no randomness, nothing fabricated.

import { fromPptx } from "@openpresentation/opf-pptx";

// Default: the OPF document, exactly as before.
const document = await fromPptx(bytes);

// Opt in: the same document, and a signals value beside it.
const { document: same, signals } = await fromPptx(bytes, { signals: true });
// or with limits: fromPptx(bytes, { signals: { maxSlides: 100, maxShapesPerSlide: 150 } })

Without signals (or with false) the return value and the document are byte for byte what they were; with it the document is identical and the result is {document, 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.

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:

  • kind: text, shape (an autoshape with no text), placeholder (an empty placeholder), picture, media (video or audio), table, chart, smartart, ole, group, connector, line or unknown (with graphicType). placeholder carries type, idx and size.
  • box: px (reference px) and emu, in slide coordinates with group transforms (offset and scale) applied, so a shape inside a scaled group has its real position. A placeholder with no box of its own gets the layout's, then the master's (box.source). rotation, flipH and flipV describe the shape; its box is the frame before rotation, and box.approximate marks a box inside a rotated or flipped group.
  • parent, children and depth give the group nesting; id is s plus the z-order index.
  • 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.
  • 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.
  • picture (part, media type, bytes, natural size, crop), table (rows, columns, widths, header flags, cell text), chart (chart types, series and point counts, title).
  • 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, slide-image, 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).

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.

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.

Chartex charts (FF-22b)

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.

The seven kept chart types with no ECMA-376 construct are written as Office 2016 chartex parts (ppt/charts/chartExN.xml, cx:chartSpace, content type application/vnd.ms-office.chartex+xml), each with its chart style and colour style parts (styleN.xml, colorsN.xml) and the embedded workbook the data came from. PptxGenJS cannot write these parts, so the exporter first writes the classic clustered column chart of the same data and then post-processes the package (src/chartex.js): the slide's chart frame becomes an mc:AlternateContent whose mc:Choice (requiring the construct's chartex namespace) references the chartex part and whose mc:Fallback keeps the classic chart frame, so a reader without chartex support still shows the data as columns. Both parts share the workbook. cx:series uniqueId values are derived from the chart number, so the bytes stay deterministic.

OPF id cx:series layoutId Data (category-major) Notes
treemap treemap [Category, Value], first series one tile per category, category data labels, one palette colour per tile
histogram clusteredColumn [Value] or [Category, Value], first series a lone value column is binned by PowerPoint with an explicit automatic bin count (Scott's rule, cx:binCount); a category column bins by category (cx:aggregation)
pareto clusteredColumn owning a paretoLine as histogram the Office Pareto chart: sorted columns plus the cumulative-percentage line on a percentage axis
box-and-whisker boxWhisker [Category, S1, S2, ...], every series rows with the same category form one box per series; exclusive quartiles, mean markers, outliers
waterfall waterfall [Category, Value], first series connector lines; increases and decreases in the first two palette colours; no subtotals
funnel funnel [Category, Value], first series value data labels
world regionMap (chartex: "native" only; Requires="cx4") [Region, Value], first series no cx:geoCache: PowerPoint accepts the part (ChartType 140) and must fetch the region shapes from its online map service; until it does it shows "There was a problem getting the information for your map chart" and draws nothing; reported as chart-map-geodata. By default ("auto") the map stays a clustered column chart

The chart area, label colour, label font (latin/ea/cs in the chart's body font) and series colours follow the classic charts. Re-import (fromPptx) reads the chartex part first: the layoutIds name the OPF id (an owned paretoLine is pareto), cx:strDim type="cat" restores the categories and each cx:numDim a series, under the same 100,000-point and 1,000,000-cell bounds as classic chart caches; a lone value column comes back as authored. A chartex part with no OPF construct (sunburst) falls back to the mc:Fallback chart, or to a text placeholder when PowerPoint's own text fallback is all there is. Native PowerPoint rendering of each construct is confirmed through the program's bounded native sample, not by this package's tests.

Languages, right-to-left text and script fonts

The exporter reads the presentation language through core resolveScriptFonts() (FF-07; the model is core docs/programs/font-fidelity-everywhere/script-font-model.md):

  • 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 the catalog's curated ooxmlLang (for example ja-JP or ar-SA) or an authored region tag such as en-NZ. altLang is not written: it names the editing-UI language, which OPF does not model.
  • 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.
  • 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.
  • Run a:ea/a:cs name the resolved slot when the language or the font scheme supplies a script-specific font, for example Meiryo in a:ea for Japanese or Arabic Typesetting in a:cs for Arabic. Headings take the heading font and other text the body font. Otherwise the slots keep repeating the run's latin face, so Latin, Cyrillic and Greek decks keep their run bytes.
  • In a right-to-left deck, each slide and notes paragraph takes its direction from core paragraphDirection(text, direction), the same rule the renderer uses: rtl="1" when its first strong character is right-to-left or it has none (digits, punctuation, empty), else an explicit rtl="0" (an English quote, a code line). The master, layout and presentation default paragraph levels start right-to-left only in a right-to-left deck. Left-to-right decks write no paragraph direction. Alignment is logical (RR-05, needs the core release that composes right-to-left decks): left is the start edge, so a right-to-left paragraph writes rtl="1" with algn="r" (PowerPoint's algn is physical, and marL/indent are the start-side margin and hanging indent, so a bullet hangs at the right), and every wrapped line shares its paragraph's direction and alignment (core computes the direction once per paragraph, not per line). The composition is mirrored through core: the left region and the first column are drawn at the right, a right-to-left table writes a:tblPr rtl="1" with its columns in logical order, column, line and area charts write c:catAx orientation maxMin (first category and value axis at the right), cover logos and header/footer zones swap sides, the master, layout and notes default levels start right-aligned, and notes paragraphs start at the right. Each Latin phrase inside an Arabic or Hebrew run (letters, joining spaces and word punctuation, digits) is written as its own en-US run, so PowerPoint orders v2.0 and PowerPoint 365 left to right instead of splitting them into items; digits that touch Arabic words stay in the Arabic run. Re-import reads right alignment of a right-to-left table cell as the logical start. Left-to-right decks are written exactly as before.
  • The PPTX names the chosen fonts only and never embeds font programs. Licensed fonts are never embedded; only open fonts could be, through the explicit FF-13 embed path. Catalog names such as Meiryo must be installed where the deck is opened.

Charts keep their c:lang and left-to-right label paragraphs. The embedded chart workbook is untouched.

When the package carries an FF-32 stored language (document round trip), 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 from the most common run lang. It uses a catalog id when that record exports the same tag (ja-JP imports as japanese, en-US as english-us). Otherwise it keeps the tag itself (en-NZ), which still resolves to its catalog record. english (en) exports en-US, so it imports as english-us. Diagnostics:

  • mixed-run-languages: runs use several tags; the most common one is imported.
  • 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.
  • language-uncatalogued: no catalog record matches; the tag is imported.
  • rtl-language-mismatch: right-to-left paragraphs under a left-to-right language.
  • script-font-not-imported: theme ea/cs name a font that neither repeats latin nor matches the language default. Imported OPF does not yet carry explicit font-scheme script slots.

Exports report language-unresolved when the document's language cannot be resolved locally (a URL, pkg: reference or unknown id) and en-US is used.

Core without the resolver. Core @openpresentation/opf 0.11.0 and earlier have no resolveScriptFonts (this release requires ^0.11.3, so this only applies to a forced older core). Export with it is byte-identical to the output before FF-07 (lang="en-US", empty theme ea/cs, no rtl), and a document that names a language gets a language-export-unavailable diagnostic. A core with the resolver but without paragraphDirection marks no paragraph direction and reports paragraph-direction-unavailable for a right-to-left deck. Import then matches run tags against the installed catalog's bcp47 and primary language. npm run test:packed exercises this path against the registry release. CI links core at a pinned commit that has the resolver.

Numbered lists (RR-33)

A list with numbering ({ "items": ["Define", "Build"], "numbering": ["arabic", { "style": "alpha-lower", "suffix": "paren" }] }) exports as native PowerPoint auto-numbers, not typed digits. Each marker line is the text box it already was, now with a:buAutoNum (arabicPeriod, arabicParenR, arabicParenBoth, romanUc/romanLc..., alphaUc/alphaLc...) whose startAt is the number core counted (written only when it is not 1), the measured marker size, colour and family (a:buClr, a:buSzPts, a:buFont) and marL/indent from core's hanging indent, so the number and the text sit where the preview draws them. A numbered list is not renumbered by PowerPoint when an entry is inserted (each line is its own box; this is how lists export today), the numbers are core's. fromPptx maps a:buAutoNum back: the scheme to a style and suffix, startAt to start, per level to an array, and a list whose numbers are not the plain count to per-entry start values; native counting follows PowerPoint (consecutive paragraphs of one scheme and start count up, a shallower or unnumbered paragraph ends the deeper sequences). The authored spelling comes back in its shortest form ("arabic" for { "style": "arabic" }). A scheme OPF has no equivalent for imports as arabic with numbering-scheme-adapted, a list that mixes numbered and bullet paragraphs imports as bullets with numbering-mixed, and different schemes at one level report numbering-style-adapted. A numbered list exported with a core that does not compose numbering reports numbering-unsupported-core and exports bullets. Lists without numbering export unchanged. See core's numbered lists.

Templates and variables (RR-32)

A deck that declares content variables, or is marked "template": true, is resolved by core resolveVariables before it is exported, so the PPTX holds exactly the text, numbers, dates and images the preview shows. Pass the values as variables:

const pptx = await toPptx(template, { variables: { client: 'Globex', revenue: 1250000 } });

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. Needs the core release that ships resolveVariables (read from the namespace, so an older core still loads and ignores the option). See templates and variables.

Runtime Policy

The package runtime must stay local and deterministic:

  • No hosted service in the critical path
  • No telemetry or hidden analytics
  • No commercial SDK dependency in the critical path
  • No required network calls
  • No required AI dependency
  • No required AI cleanup or classification pass for PPTX import
  • No required LibreOffice dependency in the runtime path; LibreOffice is allowed only as an optional verification tool in CI
  • Host applications own auth, storage, queues, analytics, collaboration, branding, and product workflow

Development

npm ci
npm run build
npm run typecheck
npm test
npm run validate

LibreOffice is not a runtime dependency. When it is installed in CI or a local verification environment, generated .pptx files can be smoke-opened there as an optional export check.

Release Lane

Public npm package publication is handled by .github/workflows/release.yml through npm Trusted Publishing (GitHub Actions OIDC) with npm provenance; no npm token is stored. The owner authorized agents to prepare and publish npm releases whenever a release is required (2026-09-29). This authorization does not waive any gate.

  1. Open a release-prep PR containing only the version bump, CHANGELOG.md (assembled from changes/ with node scripts/changelog-fragments.mjs assemble --version X.Y.Z), dependency ranges, lockfile and current-instruction docs. Publish in dependency order (core, then renderer, then PPTX, then editor): refresh this repo's lockfile only after the required @openpresentation/opf and @openpresentation/opf-render versions are on the registry (npm install --package-lock-only), then run npm run test:packed against them.
  2. Merge after CI is green, then publish by pushing the git tag opf-pptx-v<version> (or @openpresentation/opf-pptx@v<version>) at the merge commit. The workflow verifies that the tag matches package.json and reruns audit, typecheck, validate, tests, packed and browser checks before npm publish --access public --provenance. A manual workflow_dispatch runs the same job without the tag check and is a fallback only.
  3. Verify with npm view @openpresentation/opf-pptx@<version> version gitHead dist.attestations and, from the core repo, node scripts/test-pptx-publication.mjs <version> <release-commit> <this-checkout>. Never republish an existing version.

Shared dynamic composition

The current checkout uses @openpresentation/opf/composition for portable geometry. Slides can select auto, row, column, or grid, set weighted tracks, and request path-specific overflow diagnostics. See the sibling OPF repo's docs/dynamic-composition.md for the complete contract.

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.

For crowded drafts, run paginatePresentation from @openpresentation/opf/pagination 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.

Pass the same textMeasurement provider used by preview and pagination to toPptx. Plain text and headings retain the measured line breaks in editable PowerPoint shapes.

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: 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.

Since 0.5.1, the exact PptxGenJS 4.0.1 ESM distribution is shipped with its MIT license and verified upstream hashes. Its unused image-size dependency is not installed; JSZip is declared directly. See dependency provenance and regression coverage. The published 0.5.0 package retains the older dependency graph.

Native table fitting

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.

The exporter measures every cell with the same textMeasurement provider, 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.

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.

A local macOS Quick Look check opened both Roboto and system-Arial specimens. Quick Look substituted a serif font for uninstalled Roboto; the Arial specimen used a sans-serif face but still differed in table wrapping and row proportions. This is evidence of remaining viewer differences, not a passing PowerPoint raster comparison.

Image geometry

Native image exports now follow the browser's design.imageFill: fit (the default) centers an image without changing its aspect ratio, and crop fills the allocated box with a centered native crop. 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.

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.

A slide-level image (design.slideImage, composed by core as geometry.slideImage) exports as one native picture named OPF slide image slides.N. It sits beneath the slide's other shapes. Its frame is the shared composition frame for both fills: crop writes positive a:srcRect insets and fit writes negative insets that pad the centered image. The frame therefore matches the preview's <image> box exactly. An OPF_SLIDE_IMAGE_V1 shape tag records the placement and the native picture geometry. On import, an unchanged tagged picture becomes the slide's design.slideImage again, with the embedded bytes as its data URI source and imageFill: "fit" when the frame was fitted. An edited, duplicated or ambiguous tagged picture is imported as an ordinary image block and reports invalid-slide-image-provenance at slides.N.design.slideImage. Native PowerPoint raster parity for negative a:srcRect insets has not been checked with Office yet.

Slide-image treatments export from core's normalized geometry as native DrawingML:

  • shape becomes the picture's a:prstGeom (rect, roundRect, ellipse or hexagon) with core's guide values.
  • border becomes a centered solid a:ln with a miter join.
  • recolor becomes a:grayscl or a:duotone, followed by a:alphaModFix for opacity, on the blip only.
  • overlay becomes one tagged OPF slide image overlay slides.N shape directly above the picture.

A deck or slide watermark (design.watermark) exports as one native picture named OPF watermark per slide, after the slide image and its overlay, if any, and before all content (the preview's paint order, so a background slide image 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 and an object with no src reports watermark-not-exported.

Brand assets, picture bullets and the accent font (spec-gap closure A)

These design fields need the core that composes them (resolveLogo, geometry.logo, item.bulletImage, fontScheme.accent; core 0.11.4 or the coordinated source). On core 0.11.3 the export carries none of them and is byte-identical to the previous output. Each one is drawn at the box core composes, which the preview draws too (see core docs/design-resolution.md, "Brand assets and layout hints").

  • 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.
  • 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).
  • 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.
  • 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; checkPptxTypefaces callers list it in options.fonts. design.fontScheme (with accent) returns from the document tag.

An unchanged export imports every treatment field back. An edited overlay drops only the overlay and reports invalid-slide-image-provenance. An edited picture or effect drops the slide image. 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.

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.

The structural export/import corpus gate covers every installed core example (126 decks / 805 slides for core 0.4.0). It explicitly substitutes a bundled fallback font and synthetic images, then checks slide XML, unique native object IDs, finite geometry, table grids and imported slide counts. It does not establish original-asset, typography or viewer fidelity. The focused table and image tests separately exercise measured geometry and real fixture bytes.

Raster media filenames and package content types are derived from the embedded PNG/JPEG/GIF/WebP bytes. A resolver may change the format without preserving an old asset MIME hint; import likewise detects these formats from their bytes. This metadata repair does not recompress images, validate every compressed pixel stream, fetch resources or establish viewer support for each format.

Native viewer check: Keynote 14.4 displays the PNG/JPEG/GIF media-type specimens, but imports an unchanged WebP as an empty rectangle. The default compatible export now converts WebP to a static PNG locally. Keynote displays all six converted specimens, including alpha, EXIF orientation and the first animation frame. Microsoft PowerPoint has not been verified.

Compatible WebP pictures

toPptx defaults to imageFormat: "compatible". After resolving and embedding an image once, WebP bytes are decoded to a static PNG. Alpha and EXIF orientation are retained in the decoded pixels; animated input uses its first frame. Fit/crop is then calculated from the resulting PNG dimensions. The original OPF input and source bytes are unchanged, but the PPTX contains the PNG rather than the original WebP or its metadata.

Set imageFormat: "preserve" to embed WebP unchanged when the receiving application supports it. Other image formats keep their existing export behavior. Conversion errors include the OPF image path; images above 40 megapixels are rejected before compatible conversion. Decoder differences can affect color/alpha rounding, so byte identity across platforms is not promised.

Node conversion lazily loads the pinned open-source Sharp dependency and requires Node 24. Normal package installation must include platform optional dependencies for its native binaries. Browser bundles select a separate browser decoder using local Blob/image/canvas APIs; Sharp and Node code are excluded. Neither path uploads images or fetches asset URLs. Source-preserving export and ordinary PNG/JPEG/GIF operations do not load Sharp.

npm test includes the Node pixel-reference cases and verifies browser bundling. To run the browser pixel checks, run npm run build:browser-check, serve this repository locally, and open /artifacts/webp-fallback/browser/index.html. The page reports 13 checks covering embedded PNG pixels, alpha, EXIF, the first animation frame, fit/crop, resolver calls and DOM canvas fallback. These are browser export checks, separate from the recorded Keynote viewing evidence.

Native background fills

Fixed solid and linear-gradient backgrounds now export as native slide fills, keeping the background editable without rasterizing slide content. Deck defaults, inline theme overrides and per-slide overrides are resolved before export. Solid opacity, gradient stop colors/positions and combined color/background alpha are preserved. Solid, gradient-stop and pattern colors are ColorRefs, resolved like table fills and run colors: a var: variable or a colour-scheme slot or role name (accent2, primary) paints its colour, and a slot the deck theme holds exactly is written as a:schemeClr. The slide's default text contrast follows the resolved colour. Empty and single-stop gradients follow the SVG preview's transparent/solid behavior; descending stop positions clamp to the preceding stop.

Diagonal gradients require a coordinate conversion: the preview uses an SVG object-bounding-box gradient, while native unscaled DrawingML angles use slide coordinates. Export converts both the physical gradient direction and stop interval. Tests compare 990 sample positions from serialized SVG/native properties across 33 gradients and three aspect ratios, plus solid opacity, inheritance, native edits and repeated imports/exports. Integer native angles/positions introduce small rounding differences. The mapping follows the DrawingML linear-gradient angle definition.

Import reads supported native RGB solid/linear fills directly; it uses no hidden source copy. Uniform alpha becomes OPF background opacity, and differing stop alpha uses eight-bit RGBA colors (which can round alpha). Native path gradients, color transforms outside the supported luminance/alpha set, non-default tile/flip geometry and stop intervals outside OPF's fixed-endpoint representation are not imported. Pass fromPptx(bytes, {onDiagnostic: issue => ...}) to observe unsupported-background-gradient with a slide path.

Node 20/24 tests and the 126-deck / 805-slide structural corpus pass. This proves serialization and the mathematical mapping, not native viewer pixels. Keynote 14.4 recognizes the editable native gradients. Twelve captured native PNGs now support 18 comparisons, including a Keynote-generated PPTX import: opaque differences are at most 4/255 per channel (mean below 0.38), and transparent portrait alpha differs by at most 1/255. The checked-in references run in ordinary Node tests without Keynote. Quick Look still renders these specimens as a flat average color, so its thumbnails are not evidence of their native appearance. Microsoft PowerPoint remains unavailable and unverified. Theme-aware native fills and other design decorations remain separate fidelity work.

Pattern and picture backgrounds (FF-25)

Pattern backgrounds export as a native <a:pattFill> with explicit foreground and background colors. OPF presets named by DrawingML's 54 ST_PresetPatternVal values (for example pct5, ltHorz, openDmnd, wave) are written unchanged. As in the SVG preview, a missing foreground uses the slide text color and a missing background uses white; text contrast follows the pattern's background color. The preview's engine id diagStripe has no DrawingML name. It is still accepted and is written as the closest preset, wdUpDiag; new documents should use wdUpDiag so that re-import keeps the name. Any other engine-defined id keeps only its background color, like the preview, and reports unsupported-pattern. Opacity becomes alpha on both colors. Under the FF-24 theme-color rule, a pattern color authored as a scheme slot or role becomes a:schemeClr, with the background opacity as alpha, when the deck theme holds exactly the color the preview draws. Like the preview, pattern colors resolve only hex, so any other name is drawn as the default color and stays literal RGB. Import resolves theme pattern colors to RGB. The SVG preview draws every one of the 54 presets from core's 8 x 8 bitmaps (RR-07), so the preview and this a:pattFill show the same preset in the same colours (test/rr-07-preview-polish.mjs pins it for all 54); the tiles are the pixels measured from desktop PowerPoint (Office 365, Windows, 2026-10-01), not defined by ECMA-376.

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.

Image backgrounds export as a native <a:blipFill> for the embedded raster: data: sources, declared asset: ids and imageResolver results. cover (the default) crops the centered source to the slide aspect with a:srcRect. contain letterboxes the source with a:fillRect insets. tile repeats square cells that are min(width, height)/4 in size, from the top-left, as the preview does. The deck imageFill decides whether each cell is covered or contained; contained cells use negative (transparent) source insets. 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 compensates on each axis, so a cell matches the preview's CSS pixels. WebP sources become PNG parts as for pictures. Opacity becomes a:alphaModFix. An unresolved image keeps the background color and reports unresolved-asset (strictAssets throws). A JPEG EXIF orientation cannot rotate a background fill, so it reports unsupported-background-image-orientation.

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 (contained cells at the raster's resolution), the import reports approximate-background-image. A centered crop matching the slide aspect becomes cover, and centered fill insets matching the image aspect become contain. Off-center, distorted or effect-bearing fills become the closest cover and report approximate-background-image. Linked or unreadable pictures report unsupported-background-image. test/background-fills.mjs covers export, import, native edits, inheritance and diagnostics. Microsoft PowerPoint rendering of these fills, especially its resolution-based tile scale and negative tile insets, has not been verified.

Theme color scheme

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 names a catalog design.theme, a:theme and its thm15:themeFamily take that theme's name. The rewrite happens during package normalization; the vendored PptxGenJS bytes are unchanged.

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.

Default text follows the theme only where the background does. When a slide's background is an opaque theme reference (light1/light2 or dark1/dark2, the only slots a theme background accepts), the exporter's default and muted text are written as the paired slot: tx1/tx2 on bg1/bg2, and bg1/bg2 on tx1/tx2. This applies only when the resolved color is exactly that deck slot, so a PowerPoint theme switch moves text and background together. It covers headings, body and list text, list markers, metric labels, quote text and footers, header/footer text and placeholders. Text on a literal background, text on a card fill and override slides stay literal.

Table chrome follows the theme too (FF-24c). With no theme override on the slide, the default header fill is accent1, the body fill is the surface slot (bg2, or tx2 on a dark deck), and every cell border is accent5. Each is written as a:schemeClr when the deck theme slot holds exactly that color. Default cell text pairs with its fill: tx1 on a bg2 fill, bg1 on a tx2 fill, and light1 or dark1 (bg1/tx1) on an accent fill, again only when the contrast-selected color is exactly that slot. A named fill, color or border color on a cell (accent2, surface, light2, textSecondary, ...) is a scheme reference, hyperlink and followedHyperlink included, and a literal on the cell stays literal (default text on a literal fill included). PptxGenJS 4.0.1 cannot write hlink/folHlink, so those two are written as a reserved literal (FE01A0/FE01A1, or the next free pair) and rewritten to a:schemeClr in the finished slide part; a document that itself uses every reserved value keeps its link colors literal.

When the deck background is a theme slot other than light1, the slide master gets that background and its title/body/other text styles use the paired text slot. The layout drops its own bg1 background and inherits the master, so slides added in PowerPoint match the theme. light1 decks keep the vendored master.

These stay a:srgbClr:

  • literal hex colors, even when they equal a scheme slot;
  • var:<id> variables;
  • translucent colors;
  • 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.

Import reads the first slide master's theme. An exact twelve-slot match with a bundled catalog scheme returns its id; the clrScheme name breaks ties. Otherwise the importer returns inline slots, relative to the catalog scheme the clrScheme is named after when there is one ({id:'boost', accent1:'#123456'}). A theme whose name equals a catalog theme's name maps to design.theme only if the package's color scheme or heading/body fonts match that theme; otherwise theme-unverified is reported. 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 catalog schemes and 4 catalog themes, 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.

JPEG orientation on import

fromPptx now preserves native quarter-turns and mirroring for JPEG pictures by writing the combined orientation into EXIF metadata. Existing embedded EXIF orientation is applied before the native transform. This requires no pixel decoder, recompression, upload or new dependency. The original PPTX remains unchanged, and alternative text survives. The eight orientations produced by this exporter restore the exact source JPEG bytes through repeated fit-mode export/import cycles.

When a JPEG has no orientation tag, import either adds a minimal EXIF segment or appends an IFD0 that retains the existing metadata entries, referenced data offsets and next-IFD link. Malformed or full EXIF segments are left untouched and reported. Tests cover both byte orders, embedded metadata plus native transformations, native picture edits, exact compressed-byte retention and independently permuted pixels.

This preserves image orientation, not arbitrary picture geometry. Crop windows, non-quarter-turn rotations, non-JPEG rotations/mirroring and unsupported EXIF structures retain their original image bytes and report unsupported-image-crop or unsupported-image-orientation through FromPptxOptions.onDiagnostic. Picture diagnostic paths identify native picture order, for example slides.0.pictures.0. Import still recomposes OPF layout and does not promise exact native placement, crop, effects, groups or full picture round-trip fidelity. Third-party native viewers may handle already-oriented embedded JPEGs differently; the metadata-before-native composition is the importer contract, not a cross-viewer parity claim.

Inherited native backgrounds (0.2.1)

Since 0.2.1, the importer follows slide → layout → master background inheritance. An explicit slide background, including no-fill or an unsupported fill, takes precedence over inherited content. Solid and representable linear fills resolve theme slots through the master color map and layout/slide overrides; system colors use the saved lastClr fallback. Theme overrides can replace the color scheme or format scheme. Background style references use the original XML order in fillStyleLst/bgFillStyleLst, including placeholder colors and alpha. Theme-slot OPF exports also retain background opacity.

The 0.2.1 importer applies lum, lumMod, lumOff, alpha, alphaMod and alphaOff in XML order, including repeated/interleaved transforms in theme definitions, style placeholders and gradient stops. Following DrawingML’s luminance modulation and luminance offset semantics, luminance adjustments retain hue and saturation, and opacity operations clamp after each step. Colors are rounded to RGB only after the full reference/transform chain. Tint/shade, saturation/hue, gamma and other transforms still produce diagnostics. The regression suite covers 70 inheritance, transform and diagnostic cases; these mathematical tests do not establish native viewer pixel parity.

These colors become explicit editable OPF RGB fills. Import does not preserve a live link to the original PowerPoint master/theme. No external theme URL is fetched. Missing themes, unknown colors, unsupported color transforms, and unsupported fills (including patterns without a DrawingML preset or explicit colors) report unsupported-background-fill (or unsupported-background-gradient for gradients) at the slide background path.

The regression fixtures cover inheritance, overrides, interleaved style lists, repeated export/import, source preservation and observable failures. The style indexes follow the Open XML background-reference definition. They prove conversion semantics, not universal native appearance. Keynote displayed a fixture referencing fillStyleLst index 2 as white; native comparison of other reference forms is still incomplete. Microsoft PowerPoint remains unverified. This work is not included in npm 0.2.0.

Native dimensions retain full precision through import. Premature six-decimal inch rounding could change raster edges even on a 1280-pixel slide. With the local JPEG-aware renderer, all eight complete image-slide PNG previews now match their original OPF previews after native export/import; this remains an OPF-renderer comparison, not a native viewer pixel comparison.

Background-only and empty slides now remain blank during PPTX import; the importer no longer inserts a synthetic “Slide N” title. Speaker notes remain separate from visible content. Native fixture verification covers this behavior using an actual Keynote-exported presentation.

Version 0.2.0 requires Node 20.9 or later for native image decoding. Browser bundles continue using browser-safe entrypoints.

Conditional table text styles (since 0.5.0)

The importer resolves referenced table styles through the archive's presentation relationship, including custom part paths, and accepts inline definitions. It applies whole-table, alternating row/column, edge and corner text styles using Microsoft's DrawingML precedence. Direct list-level, paragraph and run properties override the inherited style. Supported character properties are bold/italic, explicit or major/minor-theme Latin fonts, and the existing RGB/system/theme color and alpha transforms. Font-reference placeholder colors resolve after style inheritance. The table-style list's insertion default does not silently restyle existing tables without a reference.

Version 0.5.0 adds conditional table styles. Missing definitions (including built-in Office IDs whose definitions are absent from the archive) produce unsupported-table-style; no style data is fetched. Unsupported effects and table backgrounds produce unsupported-table-cell-style. Supported conditional solid fills, borders and their theme references use the same precedence as character styles. Native right-to-left geometry reports unsupported-table-direction and retains source cell order. Script-specific font selection, unsupported border geometry/color models and native PowerPoint raster parity remain separate work. Native re-export preserves effective character formatting but does not reconstruct the original style reference or redundant explicit normal flags.

test/table-styles.mjs builds independent native XML fixtures covering band offsets, overlapping first/last flags, all four corners, theme and explicit fonts, placeholder alpha, inheritance/defaults, direct resets, absent/external definitions and repeated conversion. The checked-in test/fixtures/table-styles/conditional.pptx is project-authored test data, not a captured native-viewer reference. After npm run build:browser-check, serve this repository and open /artifacts/native-table-styles/browser/index.html for 11 browser import, preview-trace and re-export checks.

Styled and merged table cells (since 0.5.0)

The importer returns canonical {value, style} cell objects. Values retain supported rich runs, and styles retain direct solid fills/alpha, individual borders, horizontal/vertical alignment and reference-pixel padding. Rectangular native merges use rowSpan/colSpan anchors with explicit null at covered grid positions. Numeric source types cannot be recovered from displayed native text.

Import validates native continuation flags, bounds and covered text before applying merges. A malformed merge falls back to separate styled cells and reports unsupported-table-merge, retaining every source cell's text. If a valid merge crosses a flagged first row, the table keeps that row in its body with explicit formatting and reports table-header-in-body; OPF repeated headers cannot extend into body rows. Mixed/justified paragraph alignment, vertical text, unsupported fills/lines and 3D/diagonal effects remain explicit diagnostics. Unequal native column widths still need canonical representation.

Conditional borders keep the whole-table outer frame separate from interior horizontal/vertical lines. Row/column bands, edges and corners inherit line properties before direct cell overrides. Archive-local line references resolve theme placeholder colors and alpha. Missing references report unsupported-table-border unless an invisible or complete direct line masks them. A merged anchor uses its full span to select outer edges; differing native continuation border segments report unsupported-table-merge-border and retain the anchor border. test/table-border-styles.mjs covers these cases with native XML fixtures and repeated conversion.

Export consumes the same core geometry as SVG. It writes native merged cells and corrects PptxGenJS's padding-unit heuristic and missing dotted/transparent-border options in generated XML. Rich runs retain their own resolved opacity, including opaque overrides within a translucent cell. Tests inspect actual native XML and repeated conversions; native PowerPoint raster parity is not established.

Version 0.5.0 requires core 0.7.0 and renderer 0.5.0 for coordinated previews. npm run test:styled-table runs export/import regressions. For local browser verification, set OPF_CORE_ROOT to the core repository and OPF_RENDER_ROOT to the coordinated renderer worktree when running npm run build:browser-check. Serve the repository and open /artifacts/native-styled-table-import/browser/index.html. Omit those variables to exercise installed package dependencies.

Version 0.8.0 exports editable native lines with explicit four-space tab stops. Boundary tags contain no original source words, so native edits and deletions remain authoritative. XML-unrepresentable scalar controls reject with an actionable path rather than silently losing characters. Twenty wide/portrait measured/estimated export cases, current-text mutation controls and damaged-tag fallbacks pass; native Office acceptance remains separate. The coordinated core browser workflow also verifies editing, undo and exact reimport.

Version 0.9.1 exports core's accepted header/footer text and picture geometry as editable tagged slide shapes. Since RR-11 the footer's first text, date and slide number are native PowerPoint Header & Footer placeholders (core issue87, native header and footer); releases up to 0.11.7 wrote every part as a tagged shape. Dedicated OPF_FURNITURE_V1 tags describe roles, line boundaries, inactive flags and global/local scope. A slide manifest represents empty or disabled definitions without adding a visible shape. Tags contain no original text, image bytes or alt text. Reimport reads those values from current native shapes; complete groups recover literal dates, section/organization metadata and page-number intent when the current number matches the current slide position.

Slide numbers inside those shapes are native PowerPoint slide-number fields (<a:fld type="slidenum">), so PowerPoint renumbers them when slides move. A slideNumberFormat such as "A-{current}" or "{current} / {total}" keeps its literal text and {total} (the exported slide count) as fixed runs around the field; PowerPoint has no slide-count field. A current date (date: true) needs the host's date option (ISO YYYY-MM-DD); it becomes a native datetime1–datetime7 field when its dateFormat matches that en-US field type, and otherwise fixed text with a furniture-date-fixed diagnostic. An eligible date or number range that spans accepted text lines stays static and reports furniture-field-fixed at its source path; no native field is created by merging shapes. A string date with dateFormat is fixed text. The slide manifest records slideNumberFormat/dateFormat settings, never their rendered words: reimport keeps a format only while the current native text still matches it (a slide number at its current position, a fixed date that parses back to the same ISO date), and a live date keeps the pattern of its current field type. Otherwise the current words import as literal text. Hiding furniture on a title slide is a slide-level design.footer: false/design.header: false, which round-trips as before.

When a supported current-date field wraps across lines, full provenance records a versioned static-date reason, its format and exact line fingerprints, without cached date words. Complete unchanged soft-wrapped lines can then recover date: true and the authored format on reimport, so a later OPF export can use a new host date. The original PPTX date remains static: this does not establish native refresh or Office compatibility. Actual current native date fields take precedence. Edited or cleared text, damaged evidence, older unmarked exports, references-only and provenance: false remain literal/current-content fallbacks. Fingerprints detect changed text; writable tags are not authentication. Native formatting and geometry are not reconstructed.

Missing, duplicated or inconsistent furniture tags fall back to current native content with an invalid-furniture-provenance diagnostic. Conflicting organization or section values also fall back. Inherited definitions become global only when every slide has a valid definition/override and all inherited values agree; otherwise recovered definitions stay local. Reordering slides with unchanged visible page numbers preserves those numbers as ordinary text instead of silently renumbering them. Native typography, positioning, crop and unrelated metadata are not reconstructed; furniture-import-reflow requests review. npm run test:furniture exercises XML conversion and mutation controls. Native Office, visual corpus and clean installed-package acceptance remain separate gates; published tagged-shape furniture (0.9.1 to 0.11.7) does not establish native Header/Footer support.

Native header and footer placeholders (RR-11): the first footer text becomes a ftr placeholder, the first footer date a dt placeholder (a live datetime1-datetime7 field for date: true) and the first footer slide number an sldNum placeholder, each at core's exact box with its furniture tag; the slide master and layout carry the three placeholders and p:hf flags, and the notes master records that notes pages carry a page number only. Header parts, organization, section, socials, images, second text/date/number parts and multi-line text stay tagged shapes (PowerPoint has no object for them). fromPptx reads the placeholders back with or without provenance, and a deck changed through PowerPoint's Insert > Header & Footer dialog imports the change. Every deck, with or without a footer, carries the three master and layout placeholders (flags off when unused), so Insert > Header & Footer works on a deck with no footer. The mapping table, the dialog-edit table and the vetoable decisions are in docs/native-header-footer.md.

About

Pure local OPF to PPTX export and PPTX to OPF import tooling

Resources

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages