@openpresentation/opf-pptx 0.11.3 → 0.11.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Coordinated `@openpresentation/opf` pin
4
4
 
5
- PPTX 0.11.3 depends on published [`@openpresentation/opf@^0.11.2`](https://www.npmjs.com/package/@openpresentation/opf) (reference layer: ColorRef, document `variables`, exported `resolveColorRef()`). The optional renderer peer is [`@openpresentation/opf-render@^0.11.0`](https://www.npmjs.com/package/@openpresentation/opf-render) so editor 0.10.0 + render 0.11.0 + pptx 0.11.0 install together. CI checks out OPF [`b8a1faf`](https://github.com/OpenPresentation/opf/commit/b8a1faf24203635bd36b09ea19f44bcd53781f16) (`opf-v0.11.0`) for coordinated source linking. Named content colors resolve through core `resolveColorRef()`; eight-digit hex alpha stays a local export concern because core `normalizeHexColor` drops the alpha byte. The deck color scheme is written as the theme `clrScheme`, and named slot and role colors (text, table cells and borders, hyperlinks, backgrounds) are written as `schemeClr` where the deck theme holds exactly that color; card, timeline and media colors, literal hex, variables, translucent colors and slide-override colors stay literal `srgbClr`.
5
+ PPTX 0.11.4 depends on published [`@openpresentation/opf@^0.11.3`](https://www.npmjs.com/package/@openpresentation/opf) (reference layer: ColorRef, document `variables`, exported `resolveColorRef()`). The optional renderer peer is [`@openpresentation/opf-render@^0.11.0`](https://www.npmjs.com/package/@openpresentation/opf-render) so editor 0.10.0 + render 0.11.0 + pptx 0.11.0 install together. CI checks out OPF [`b8a1faf`](https://github.com/OpenPresentation/opf/commit/b8a1faf24203635bd36b09ea19f44bcd53781f16) (`opf-v0.11.0`) for coordinated source linking. Named content colors resolve through core `resolveColorRef()`; eight-digit hex alpha stays a local export concern because core `normalizeHexColor` drops the alpha byte. The deck color scheme is written as the theme `clrScheme`, and named slot and role colors (text, table cells and borders, hyperlinks, backgrounds) are written as `schemeClr` where the deck theme holds exactly that color; card, timeline and media colors, literal hex, variables, translucent colors and slide-override colors stay literal `srgbClr`.
6
6
 
7
7
  OPF PPTX 0.5.1 ships the exact, unmodified PptxGenJS 4.0.1 ESM distribution in `vendor/pptxgenjs`, with its MIT license, upstream archive integrity and per-file SHA-256 hashes. Its actual JSZip dependency is declared directly. Ordinary npm installations therefore omit the unused image-size parser without requiring consumer overrides. This removes the affected dependency; it does not patch the parser. Published OPF PPTX 0.5.0 retains the older graph.
8
8
 
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # OPF PPTX
2
2
 
3
- 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).
3
+ 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).
4
4
 
5
5
  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.
6
6
 
@@ -24,7 +24,7 @@ Version 0.8.0 also measures `design.contentBox` cards through core's padded inte
24
24
 
25
25
  ## Scope
26
26
 
27
- 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](docs/evidence/shared-quote-integration/comparison.json) records glyph containment, separation, save/reopen and text reimport against its exact source/font hashes. Native quote import returns editable text blocks and does not restore the original OPF quote structure, typography or readability policy. Native chart geometry and general scalar-text wrapping also remain different from preview; editability and valid reimport do not establish raster equivalence.
27
+ 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](docs/evidence/shared-quote-integration/comparison.json) 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](#quote-provenance-ff-57)); 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.
28
28
 
29
29
  - Package: `@openpresentation/opf-pptx`
30
30
  - Repository: `OpenPresentation/opf-pptx`
@@ -84,8 +84,8 @@ The first exporter keeps the public API stable while using `pptxgenjs` internall
84
84
  - A chart whose data is one column of values (a histogram or dot plot) has no category column. It exports as a native chart and reports `chart-data-adapted`: by default 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), and any other chart type plots the values against their row numbers (`row-numbers`). With `chartex: "native"` 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. 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.
85
85
  - 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.
86
86
  - ZIP entries, generated chart/workbook part names, core-property timestamps, and nested chart workbook timestamps are normalized for reproducible bytes. [Export determinism](docs/export-determinism.md) lists the tested time zone, locale, clock and host-font controls and each known variance (WebP conversion, font registries, ICU segmentation, timestamps).
87
- - 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.
88
- - `checkPptxTypefaces(bytes, {fonts, monospace})` 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.
87
+ - The package names only the document's chosen fonts. Chart text (data labels, axes, legend, titles) uses the chart slide's body font in `latin`/`ea`/`cs`; each embedded chart workbook uses the same fonts in its styles and theme; run `pitchFamily` follows the font scheme type (monospace is fixed pitch, serif is roman); and `docProps/app.xml` "Fonts Used" lists the fonts the package actually uses. The theme keeps PptxGenJS's per-script supplements (`THEME_SCRIPT_SUPPLEMENTS`) and empty `ea`/`cs` slots unless a script font was selected for that slot (see [Languages, right-to-left text and script fonts](#languages-right-to-left-text-and-script-fonts)).
88
+ - `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.
89
89
 
90
90
  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.
91
91
 
@@ -101,6 +101,12 @@ This intentionally tightens the previous host-dependent `Date` parsing contract.
101
101
 
102
102
  ## v1 Import Mapping
103
103
 
104
+ ### Quote provenance (FF-57)
105
+
106
+ 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.
107
+
108
+ 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.
109
+
104
110
  ### New in 0.10.0: current native body formatting
105
111
 
106
112
  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.
@@ -148,7 +154,7 @@ The chart area, label colour, label font (`latin`/`ea`/`cs` in the chart's body
148
154
  The exporter reads the presentation `language` through core `resolveScriptFonts()` (FF-07; the model is core `docs/programs/font-fidelity-everywhere/script-font-model.md`):
149
155
 
150
156
  - 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.
151
- - For a language that uses an East Asian or complex-script slot (CJK, Arabic, Hebrew, Indic, Thai and others), theme major/minor `a:ea`/`a:cs` name the resolved heading/body fonts instead of the vendored empty values. For a language written in the latin slot (Latin, Cyrillic, Greek and others) they stay empty unless the design font scheme sets `eastAsian`/`complexScript` explicitly. Filling them with the latin family is gated on FF-05: native evidence shows it does not remove the nameless and Aptos entries PowerPoint lists at open, and empty slots keep PowerPoint's per-script theme fallback for CJK or Arabic text typed later. So en-US decks export byte-identically.
157
+ - 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.
152
158
  - 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.
153
159
  - 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.
154
160
  - 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 unchanged: `algn` stays the composed absolute alignment (left stays `l`), so native line placement keeps matching the renderer's geometry. Right-aligning RTL text by default is a composition decision in core; until then, set `contentAlignment` or `titleAlignment` to `right`.
@@ -166,7 +172,7 @@ When the package carries an FF-32 stored `language` ([document round trip](docs/
166
172
 
167
173
  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.
168
174
 
169
- **Core without the resolver.** Core `@openpresentation/opf` 0.11.0 and earlier have no `resolveScriptFonts` (this release requires ^0.11.2, 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.
175
+ **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.
170
176
 
171
177
  ## Runtime Policy
172
178
 
package/dist/index.js CHANGED
@@ -12,6 +12,7 @@ import {attachMediaTags,importMediaGroups,mediaCaption,mediaFrameRecord,mediaTex
12
12
  import {attachHeadingTags,importHeadingGroups} from './heading-provenance.js';
13
13
  import {attachPlainTextTags,importPlainTextGroups} from './text-provenance.js';
14
14
  import {attachTimelineTags,timelineManifest,importTimelineGroups} from './timeline-provenance.js';
15
+ import {attachQuoteTags,quoteManifest,importQuoteGroups} from './quote-provenance.js';
15
16
  import {attachFurnitureTags, furnitureManifest, importFurniture, staticDateFallback} from './furniture-provenance.js';
16
17
  import {applyDocumentProvenance, attachDocumentProvenance, documentProvenance, restoreDocumentProvenance} from './document-provenance.js';
17
18
  import {attachFurnitureFields, furniturePartFields, lineFields, nativeFieldType} from './furniture-fields.js';
@@ -218,6 +219,7 @@ export async function toPptx(input, options = {}) {
218
219
  context.headingTags = new Map();
219
220
  context.plainTextTags = new Map();
220
221
  context.timelineTags = new Map();
222
+ context.quoteTags = new Map();
221
223
  context.furnitureTags = new Map();
222
224
  context.furnitureFields = new Map();
223
225
  context.furnitureManifests = new Map();
@@ -520,6 +522,7 @@ function importSlide(entries, slidePath, slideIndex, presentationDimensions, opt
520
522
  }),
521
523
  diagnostic => options.onDiagnostic?.({...diagnostic, path: slideImagePath}));
522
524
  if (slideImage.design) slide.design = {...slide.design, ...slideImage.design};
525
+ if (slideImage.image) slide.image = slideImage.image;
523
526
  nativeContext.slideImagePictures = slideImage.consumed;
524
527
  nativeContext.slideImageShapes = slideImage.consumedShapes;
525
528
  const watermarkPath = `slides.${slideIndex}.design.watermark`;
@@ -584,6 +587,7 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
584
587
  const headings=importHeadingGroups(shapes,paragraphs,relationships,entries,diagnostic=>options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}`}));
585
588
  const plainText=importPlainTextGroups(shapes,paragraphs,relationships,entries,diagnostic=>options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}`}));
586
589
  const timelines=importTimelineGroups(shapes,paragraphs,relationships,entries,diagnostic=>options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}`}));
590
+ const quotes=importQuoteGroups(shapes,paragraphs,relationships,entries,diagnostic=>options.onDiagnostic?.({...diagnostic,path:`slides.${slideIndex}.quote`}));
587
591
  for(const group of [...headings.items,...plainText.items]) {
588
592
  const item=importShape(group.shapes[0],dimensions,group.paragraphs,true);
589
593
  if(item){
@@ -597,12 +601,17 @@ function collectSlideItems(entries, slideRoot, slidePath, relationships, dimensi
597
601
  let union;for(const bound of bounds){if(!union)union={...bound};else{const x=Math.min(union.x,bound.x),y=Math.min(union.y,bound.y);union={x,y,w:Math.max(union.x+union.w,bound.x+bound.w)-x,h:Math.max(union.y+union.h,bound.y+bound.h)-y};}}
598
602
  items.push({kind:'timeline',sourceText:true,bounds:union,payload:group.payload});
599
603
  }
604
+ for(const group of quotes.items){
605
+ const bounds=group.shapes.map(shape=>shapeBounds(shape['p:spPr']?.['a:xfrm'])).filter(Boolean);
606
+ let union;for(const bound of bounds){if(!union)union={...bound};else{const x=Math.min(union.x,bound.x),y=Math.min(union.y,bound.y);union={x,y,w:Math.max(union.x+union.w,bound.x+bound.w)-x,h:Math.max(union.y+union.h,bound.y+bound.h)-y};}}
607
+ items.push({kind:'quote',bounds:union,payload:group.payload});
608
+ }
600
609
  for (const item of code.items) items.push({kind:'code',bounds:shapeBounds(item.shape['p:spPr']?.['a:xfrm']),payload:item.payload});
601
610
  for (const item of metric.items) items.push({kind:'metric',bounds:shapeBounds(item.shape['p:spPr']?.['a:xfrm']),payload:item.payload});
602
611
  for (const item of media.items) items.push({kind:'media',sourceText:item.payload.type==='text',bounds:shapeBounds(item.shape['p:spPr']?.['a:xfrm']),payload:item.payload});
603
612
  for (const [index,shape] of shapes.entries()) {
604
613
  if (furniture.text.has(index) || nativeContext.slideImageShapes?.has(index)) continue;
605
- if (code.consumed.has(shape)||metric.consumed.has(shape)||cards.has(shape)||media.consumed.has(shape)||headings.consumed.has(shape)||plainText.consumed.has(shape)||timelines.consumed.has(shape)) continue;
614
+ if (code.consumed.has(shape)||metric.consumed.has(shape)||cards.has(shape)||media.consumed.has(shape)||headings.consumed.has(shape)||plainText.consumed.has(shape)||timelines.consumed.has(shape)||quotes.consumed.has(shape)) continue;
606
615
  const ordinaryBody = Object.hasOwn(shape, 'p:txBody') && !shape['p:nvSpPr']?.['p:nvPr']?.['p:custDataLst']?.['p:tags'];
607
616
  const item = importShape(shape, dimensions, paragraphs[index], ordinaryBody || furniture.taggedText.has(index) || media.captionShapes.has(shape));
608
617
  if (item && ordinaryBody) item.readNativeBody = () => readBody(index);
@@ -1461,7 +1470,7 @@ async function addPayload(slide, presentation, payload, region, path, context, o
1461
1470
  addMetricPayload(slide, payload.metric,metricLayout,context,path);
1462
1471
  break;
1463
1472
  case "quote":
1464
- addQuotePayload(slide, quoteLayout, context, options, path);
1473
+ addQuotePayload(slide, payload.quote, quoteLayout, context, options, path);
1465
1474
  break;
1466
1475
  case "timeline":
1467
1476
  addTimelinePayload(slide, payload.timeline, timelineLayout, context, options, path);
@@ -1641,7 +1650,9 @@ async function addSlideImage(slide, presentation, image, slideIndex, context, op
1641
1650
  : image.recolor?.type === 'duotone' ? { type: 'duotone', dark: paint(image.recolor.dark, '000000'), light: paint(image.recolor.light, 'FFFFFF') } : null,
1642
1651
  overlay: image.overlay ? { ...paint(image.overlay.color, context.colors.text), opacity: image.overlay.opacity, box: image.overlay.box, shape: image.overlay.shape } : null,
1643
1652
  };
1644
- context.slideImages.set(objectName, { slide: `slides.${slideIndex}`, box, fill: image.fill, path: image.sourcePath, treatment: { ...treatment, position: image.position }, effects });
1653
+ // FF-53: a root `image` with the slide image's source is the slide image (core `replacesContent`); the
1654
+ // manifest records that so an unchanged picture imports back as both design.slideImage and slide.image.
1655
+ context.slideImages.set(objectName, { slide: `slides.${slideIndex}`, box, fill: image.fill, path: image.sourcePath, treatment: { ...treatment, position: image.position }, effects, content: image.replacesContent === true });
1645
1656
  slide.addImage({ ...resolved, objectName, ...box, altText: image.alt ?? assetAlt(image.value, presentation) });
1646
1657
  // The overlay scrim is a separate native shape directly above the picture.
1647
1658
  if (effects.overlay) {
@@ -1832,12 +1843,13 @@ function addMeasuredPayloadText(slide, text, box, context, options, config) {
1832
1843
  for (const [index, line] of fit.lines.entries()) {
1833
1844
  const link = config.links?.[sourceLineIndex];
1834
1845
  if (fit.sourceLines?.[index]?.boundary === 'hard') sourceLineIndex += 1;
1835
- if (!line&&!config.heading&&!config.sourceText&&!config.timeline&&!config.keepEmpty) continue;
1846
+ if (!line&&!config.heading&&!config.sourceText&&!config.timeline&&!config.quote&&!config.keepEmpty) continue;
1836
1847
  const alignment=fit.placement?.alignment??config.align??context.contentAlignment??'left',placed=fit.placement?.lines[index],factor=alignment==='right'?1:alignment==='center'?.5:0;
1837
1848
  const sourceLine=fit.sourceLines?.[index],boundary=sourceLine?{boundary:sourceLine.boundary,separator:String(text).slice(sourceLine.end,sourceLine.nextStart)}:{};
1838
- const objectName=config.heading?`OPF heading ${config.path} line ${index}`:config.timeline?`OPF timeline ${config.timeline.group} part ${config.timeline.part} line ${index}`:config.sourceText?`OPF text ${config.path} line ${index}`:config.objectName?`${config.objectName} line ${index}`:undefined;
1849
+ const objectName=config.heading?`OPF heading ${config.path} line ${index}`:config.timeline?`OPF timeline ${config.timeline.group} part ${config.timeline.part} line ${index}`:config.quote?`OPF quote ${config.quote.group} part ${config.quote.part} line ${index}`:config.sourceText?`OPF text ${config.path} line ${index}`:config.objectName?`${config.objectName} line ${index}`:undefined;
1839
1850
  if(config.heading)context.headingTags.set(objectName,{v:1,group:config.path,field:config.heading,line:index,count:fit.lines.length,...boundary});
1840
1851
  else if(config.timeline)context.timelineTags.set(objectName,{v:1,role:'text',group:config.timeline.group,part:config.timeline.part,line:index,count:fit.lines.length,...boundary,...(config.timeline.part===0&&index===0?{anchor:config.timeline.anchor}:{})});
1852
+ else if(config.quote)context.quoteTags.set(objectName,{v:1,role:'text',group:config.quote.group,part:config.quote.part,line:index,count:fit.lines.length,...boundary,...(config.quote.part===0&&index===0?{anchor:config.quote.anchor}:{})});
1841
1853
  else if(config.furniture){
1842
1854
  context.furnitureTags.set(objectName,{v:1,role:'text',...config.furniture,line:index,count:fit.lines.length,...boundary});
1843
1855
  if(config.liveFields?.[index]?.length)context.furnitureFields.set(objectName,{text:line,fields:config.liveFields[index]});
@@ -1956,13 +1968,16 @@ function addMetricPayload(slide,value,layout,context,path) {
1956
1968
  }
1957
1969
  }
1958
1970
 
1959
- function addQuotePayload(slide, layout, context, options, path) {
1971
+ function addQuotePayload(slide, value, layout, context, options, path) {
1960
1972
  if (!layout) throw new OPFPptxError('missing-quote-layout', 'Quote export requires a coordinated core build with shared quote geometry.', {path});
1961
- for (const part of layout.parts) {
1962
- if (!part.fit) throw new OPFPptxError('layout-overflow', 'Quote content has no usable internal space; increase its cell size before exporting.', {path:part.path,issues:layout.diagnostics});
1973
+ for (const part of layout.parts) if (!part.fit) throw new OPFPptxError('layout-overflow', 'Quote content has no usable internal space; increase its cell size before exporting.', {path:part.path,issues:layout.diagnostics});
1974
+ // Every line shape is tagged (OPF_QUOTE_V1) so an unchanged export re-imports as a quote payload (FF-57).
1975
+ const group=String(context.quoteTags.size),anchor=quoteManifest(value,layout);
1976
+ for (const [index,part] of layout.parts.entries()) {
1963
1977
  addMeasuredPayloadText(slide,part.text,part.box,context,options,{
1964
1978
  path:part.path,fit:part.fit,textStyle:part.style,diagnosticsHandled:true,
1965
1979
  color:part.role==='footer'?context.mutedColor:context.textColor,
1980
+ quote:{group,part:index,anchor},
1966
1981
  });
1967
1982
  }
1968
1983
  }
@@ -2522,6 +2537,7 @@ async function normalizePptxZip(raw, context) {
2522
2537
  attachHeadingTags(entries,context.headingTags);
2523
2538
  attachPlainTextTags(entries,context.plainTextTags);
2524
2539
  attachTimelineTags(entries,context.timelineTags);
2540
+ attachQuoteTags(entries,context.quoteTags);
2525
2541
  attachFurnitureFields(entries,context.furnitureFields);
2526
2542
  attachFurnitureTags(entries,context.furnitureTags,context.furnitureManifests);
2527
2543
  for(const [part,bytes]of Object.entries(entries)){
@@ -0,0 +1,89 @@
1
+ import {XMLParser} from 'fast-xml-parser';
2
+ import {validatePresentation} from '@openpresentation/opf';
3
+ import {attachTextTags,decodeTextTag} from './code-provenance.js';
4
+ import {sourceLineParagraphs} from './text-provenance.js';
5
+
6
+ // FF-57: a `quote` payload exports as native text lines (a body part and an optional footer part). Each line shape carries an
7
+ // OPF_QUOTE_V1 tag, so an unchanged export imports back as {quote: ...} instead of loose text blocks. Like timelines, only topology
8
+ // and line boundaries are stored: every word comes from the current native text, so a cleared or edited quote cannot bring back
9
+ // old words, and a group that no longer matches its manifest degrades to ordinary text blocks with a diagnostic.
10
+ const TAG='OPF_QUOTE_V1',REL='http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags';
11
+ const SEPARATOR=' - ',FIELDS=['attribution','source'],FOOTERS=['["attribution"]','["source"]','["attribution","source"]'];
12
+ const parser=new XMLParser({ignoreAttributes:false,attributeNamePrefix:'',parseTagValue:false,trimValues:false});
13
+ const decoder=new TextDecoder('utf-8',{fatal:true}),array=value=>value===undefined?[]:Array.isArray(value)?value:[value];
14
+ export const attachQuoteTags=(entries,records)=>attachTextTags(entries,records,TAG,'opfQuote','quote');
15
+
16
+ // The manifest rides on the first body line. `footer` lists which fields the footer part joins (core writes "attribution - source",
17
+ // skipping an absent field) and, when both are present, where the separator starts, so an unchanged footer splits back exactly.
18
+ export function quoteManifest(value,layout) {
19
+ const shorthand=typeof value==='string',fields=shorthand?[]:FIELDS.filter(field=>value[field]);
20
+ return {shorthand,...(fields.length?{footer:{fields,...(fields.length===2?{split:value.attribution.length}:{})}}:{}),
21
+ parts:layout.parts.map(part=>({role:part.role,lines:part.fit.sourceLines.length}))};
22
+ }
23
+ function validateManifest(manifest) {
24
+ if(!manifest||typeof manifest.shorthand!=='boolean'||!Array.isArray(manifest.parts)||manifest.parts.length<1||manifest.parts.length>2)throw Error('Invalid quote topology.');
25
+ const roles=manifest.parts.map(part=>part?.role);
26
+ if(roles[0]!=='body'||(roles.length===2&&roles[1]!=='footer'))throw Error('Invalid quote parts.');
27
+ let count=0;
28
+ for(const part of manifest.parts){
29
+ if(!Number.isSafeInteger(part.lines)||part.lines<1||part.lines>10000)throw Error('Invalid quote line count.');
30
+ count+=part.lines;
31
+ }
32
+ if(count>10000)throw Error('Too many quote source lines.');
33
+ const footer=manifest.footer;
34
+ if(roles.length===2){
35
+ if(manifest.shorthand||!footer||!FOOTERS.includes(JSON.stringify(footer.fields)))throw Error('Invalid quote footer fields.');
36
+ if(footer.fields.length===2?!Number.isSafeInteger(footer.split)||footer.split<1:footer.split!==undefined)throw Error('Invalid quote footer split.');
37
+ }else if(footer!==undefined)throw Error('Invalid quote footer.');
38
+ return count;
39
+ }
40
+
41
+ const unwrap=text=>text.length>=2&&text.startsWith('"')&&text.endsWith('"')?text.slice(1,-1):text;
42
+ // Split the current footer text back into fields. A footer edited so the recorded separator no longer matches splits at its
43
+ // first separator; one with none keeps everything as the attribution.
44
+ function footerFields(text,footer,report) {
45
+ if(footer.fields.length===1)return {[footer.fields[0]]:text};
46
+ const at=text.startsWith(SEPARATOR,footer.split)?footer.split:text.indexOf(SEPARATOR);
47
+ if(at<0){
48
+ report({code:'quote-footer-merged',message:'The quote footer no longer separates attribution from source; the current text is kept as the attribution.'});
49
+ return {attribution:text};
50
+ }
51
+ return {attribution:text.slice(0,at),source:text.slice(at+SEPARATOR.length)};
52
+ }
53
+
54
+ export function importQuoteGroups(shapes,paragraphs,relationships,entries,report) {
55
+ const groups=new Map(),consumed=new Set(),items=[];
56
+ for(const [index,shape]of shapes.entries()){
57
+ const tags=[];let unreadable=false;
58
+ for(const link of array(shape['p:nvSpPr']?.['p:nvPr']?.['p:custDataLst']?.['p:tags'])){
59
+ const rel=relationships.get(link['r:id']);if(rel?.type!==REL||rel.targetMode==='External'||!entries[rel.path])continue;
60
+ try{tags.push(...array(parser.parse(decoder.decode(entries[rel.path]))['p:tagLst']?.['p:tag']));}catch{unreadable=true;}
61
+ }
62
+ for(const tag of tags.filter(tag=>tag.name?.toUpperCase()===TAG))try{
63
+ const data=decodeTextTag(tag.val);if(data?.v!==1||typeof data.group!=='string'||!/^\d{1,12}$/.test(data.group))throw Error('Invalid quote identity.');
64
+ const group=groups.get(data.group)??[];group.push({shape,index,data,ambiguous:unreadable||tags.filter(tag=>/^OPF_/i.test(tag.name)).length!==1});groups.set(data.group,group);
65
+ }catch{report({code:'invalid-quote-provenance',message:'Invalid quote tags; ordinary import retains current native text.'});}
66
+ }
67
+ for(const group of groups.values())try{
68
+ const anchors=group.filter(item=>item.data.anchor!==undefined);if(anchors.length!==1)throw Error('Missing or duplicated quote anchor.');
69
+ const anchor=anchors[0],manifest=anchor.data.anchor,count=validateManifest(manifest);
70
+ if(anchor.data.role!=='text'||anchor.data.part!==0||anchor.data.line!==0||group.length!==count||new Set(group.map(item=>item.shape)).size!==count)throw Error('Incomplete or duplicated quote lines.');
71
+ const ordered=manifest.parts.map(part=>Array(part.lines));
72
+ for(const item of group){
73
+ if(item.ambiguous)throw Error('Multiple source identities on one quote shape.');
74
+ const {part,line}=item.data;
75
+ if(item.data.role!=='text'||!Number.isSafeInteger(part)||!Number.isSafeInteger(line)||part<0||line<0||!manifest.parts[part]||line>=manifest.parts[part].lines||item.data.count!==manifest.parts[part].lines||ordered[part][line])throw Error('Ambiguous quote source line.');
76
+ ordered[part][line]=item;
77
+ }
78
+ const [body,footer]=ordered.map(lines=>sourceLineParagraphs(lines,paragraphs)[0].text),local=[];
79
+ let quote={text:unwrap(body)};
80
+ if(footer)quote={...quote,...footerFields(footer,manifest.footer,diagnostic=>local.push(diagnostic))};
81
+ if(manifest.shorthand&&Object.keys(quote).length===1)quote=quote.text;
82
+ if(!validatePresentation({slides:[{quote}]}).valid)throw Error('Current native text does not form a valid quote.');
83
+ for(const item of group)consumed.add(item.shape);
84
+ items.push({shapes:group.map(item=>item.shape),payload:{type:'quote',quote}});
85
+ for(const diagnostic of local)report(diagnostic);
86
+ report({code:'quote-import-reflow',message:'Quote text, attribution and source were recovered from the current native text. Native formatting, positions and font theme are not reconstructed; review the reflowed quote.'});
87
+ }catch(error){report({code:'invalid-quote-provenance',message:`${error.message} Ordinary import retains current native text without restoring old source words.`});}
88
+ return {consumed,items};
89
+ }
@@ -64,23 +64,23 @@ const escapeAttribute = value => String(value).replace(/[&<>"']/g, char => ({"&"
64
64
  * language's own per-script supplement. Only the supplement's script entry
65
65
  * changes; the rest of the vendored per-script list is FF-08's call.
66
66
  *
67
- * For a language written in the latin slot (Latin, Cyrillic, Greek and others) the
68
- * vendored empty ea/cs stay empty unless the design font scheme sets that
69
- * slot explicitly. Filling them with the latin family is gated on FF-05 (core
70
- * script-font-model.md): it did not remove PowerPoint's nameless/Aptos font
71
- * entries, and empty slots keep PowerPoint's per-script theme fallback for
72
- * East Asian or complex-script text typed later. Other languages fill both.
67
+ * FF-49: a slot is written only when a script font is actually selected for it,
68
+ * that is when the resolver's source for that slot is not `latin`: the design
69
+ * font scheme's explicit `eastAsian`/`complexScript`, the scheme's own script
70
+ * family, or the language's script font (Model C). Every other slot keeps the
71
+ * vendored empty typeface, exactly as Office's own themes leave it, so PowerPoint
72
+ * picks its per-language default for script text typed later and the package
73
+ * names nothing the author did not select (core script-font-model.md, "Theme
74
+ * slots (FF-49)"). Latin, Cyrillic and Greek decks keep both slots empty, and a
75
+ * Japanese deck writes `ea` only.
73
76
  */
74
77
  export function themeScriptFonts(xml, plan) {
75
78
  const {heading, body, supplement} = plan.deck;
76
79
  for (const [tag, slots, family] of [["majorFont", heading, supplement?.heading], ["minorFont", body, supplement?.body]]) {
77
80
  xml = xml.replace(new RegExp(`<a:${tag}>[\\s\\S]*?</a:${tag}>`), block => {
78
- // With no script-specific choice the slot repeats the theme's own latin
79
- // face exactly as written, as the run slots do.
80
- const latin = /<a:latin typeface="([^"]*)"/.exec(block)?.[1];
81
81
  for (const [element, slot] of SCRIPT_SLOTS) {
82
- if (plan.deck.scriptRole === "latin" && plan.deck.sources[slot] !== "fontScheme") continue;
83
- const face = plan.deck.sources[slot] === "latin" && latin ? latin : escapeAttribute(slots[slot]);
82
+ if (plan.deck.sources[slot] === "latin") continue;
83
+ const face = escapeAttribute(slots[slot]);
84
84
  block = block.replace(new RegExp(`<a:${element}\\b[^>]*/>`), `<a:${element} typeface="${face}"/>`);
85
85
  }
86
86
  if (supplement && family) {
@@ -71,6 +71,7 @@ export function placeSlideImages(entries, images, metadataFor, fail) {
71
71
  }
72
72
  const node = parser.parse(picture)['p:pic'];
73
73
  manifests.set(name, {v:1, slide:image.slide, position:image.treatment.position, fill:image.fill, treatment:image.treatment,
74
+ ...(image.content === true ? {content:true} : {}),
74
75
  properties:node['p:spPr'], blipFill:blipFillIdentity(node['p:blipFill'])});
75
76
  return picture;
76
77
  });
@@ -129,6 +130,7 @@ export function importSlideImage(pictures, shapes, relationships, entries, slide
129
130
  if (unreadable || others || own.length !== 1) throw Error('Ambiguous slide image identity.');
130
131
  const manifest = decodeTextTag(own[0].val);
131
132
  if (manifest?.v !== 1 || manifest.slide !== `slides.${slideIndex}` || !['crop', 'fit'].includes(manifest.fill) || !validTreatment(manifest.treatment, manifest.position)) throw Error('Invalid slide image manifest.');
133
+ if (manifest.content !== undefined && manifest.content !== true) throw Error('Invalid slide image content flag.');
132
134
  if (picture['p:nvPicPr']?.['p:cNvPr']?.name !== slideImageName(manifest.slide)) throw Error('Slide image identity changed.');
133
135
  if (canonical(picture['p:spPr']) !== canonical(manifest.properties) || canonical(blipFillIdentity(picture['p:blipFill'])) !== canonical(manifest.blipFill)) throw Error('Slide image geometry or effects changed.');
134
136
  const item = readPicture(picture);
@@ -144,7 +146,11 @@ export function importSlideImage(pictures, shapes, relationships, entries, slide
144
146
  report({code:'invalid-slide-image-provenance', message:'The tagged OPF slide image overlay is missing, edited or ambiguous. The slide image is recovered without its overlay; any remaining native shape is imported as ordinary content.'});
145
147
  } else consumedShapes.add(overlay);
146
148
  }
147
- return {consumed, consumedShapes, design: {slideImage: {...treatment, src}, ...(manifest.fill === 'fit' ? {imageFill: 'fit'} : {})}};
149
+ // FF-53: a picture that replaced the slide's root `image` (same source as the slide image) restores that
150
+ // payload too, so the round trip keeps `slide.image` with its alt text as well as design.slideImage.
151
+ const alt = item.payload.image.alt;
152
+ const content = manifest.content === true ? {image: {src, ...(typeof alt === 'string' && alt ? {alt} : {})}} : {};
153
+ return {consumed, consumedShapes, design: {slideImage: {...treatment, src}, ...(manifest.fill === 'fit' ? {imageFill: 'fit'} : {})}, ...content};
148
154
  } catch {
149
155
  invalid();
150
156
  return {consumed, consumedShapes};
@@ -9,7 +9,7 @@ import {unzipSync} from 'fflate';
9
9
  // rich-text `rFont`) and the docProps/app.xml "Fonts Used" list.
10
10
  // checkPptxTypefaces() then applies the owner font policy: a package names only
11
11
  // the fonts its author chose, plus theme references that resolve to them, empty
12
- // theme script slots (FF-05) and the documented theme script supplements.
12
+ // theme script slots when no script font was selected (as in Office's themes; FF-49 checks a selected slot through options.themeScripts) and the documented theme script supplements.
13
13
 
14
14
  const decoder = new TextDecoder();
15
15
  const NESTED_PACKAGE = /\.(?:xlsx|xlsm|docx|pptx)$/i;
@@ -96,8 +96,18 @@ export function checkPptxTypefaces(input, options = {}) {
96
96
  const violations = [];
97
97
  const fail = (entry, reason, details = {}) => violations.push({reason, part: entry.part, element: entry.element, typeface: entry.typeface, ...details});
98
98
  const emptyAllowed = entry => allowEmpty && entry.theme && (entry.element === 'ea' || entry.element === 'cs');
99
+ // FF-49: options.themeScripts names the East Asian / complex-script families the author selected for the
100
+ // presentation theme, {major: {ea, cs}, minor: {ea, cs}}. A selected slot must carry exactly that family (an
101
+ // empty or different one is a violation); a slot with nothing selected may stay empty, as in Office's own themes.
102
+ const themeSlot = entry => entry.theme && !entry.part.includes('!/') && (entry.element === 'ea' || entry.element === 'cs')
103
+ ? options.themeScripts?.[entry.theme]?.[entry.element] || '' : '';
99
104
  const pitches = new Map();
100
105
  for (const entry of inventory.typefaces) {
106
+ const wanted = themeSlot(entry);
107
+ if (wanted && entry.typeface !== wanted) {
108
+ fail(entry, 'theme-script-slot', {expected: wanted});
109
+ continue;
110
+ }
101
111
  const reference = THEME_REFERENCE.exec(entry.typeface);
102
112
  if (reference) {
103
113
  if (entry.resolved === null) fail(entry, 'unresolved-theme-reference');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openpresentation/opf-pptx",
3
- "version": "0.11.3",
3
+ "version": "0.11.4",
4
4
  "description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,8 +37,8 @@
37
37
  "sideEffects": false,
38
38
  "scripts": {
39
39
  "build": "node scripts/build.mjs",
40
- "typecheck": "node --check src/index.js && node --check src/chart-types.js && node --check src/chartex.js && node --check test/chartex.mjs && node --check test/chart-types.mjs && node --check test/design-alignment.mjs && node --check src/color-ref.js && node --check src/theme-colors.js && node --check test/theme-colors.mjs && node --check test/table-theme-colors.mjs && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && node --check src/slide-image-provenance.js && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check src/watermark-provenance.js && node --check src/media-dedupe.js && node --check test/media-dedupe.mjs && node --check test/watermark.mjs && node --check test/image-fit.mjs && node --check test/image-orientation.mjs && node --check test/table-layout.mjs && node --check test/table-import.mjs && node --check test/object-ids.mjs && node --check test/export-corpus.mjs && node --check test/image-media-type.mjs && node --check src/image-fallback-node.js && node --check src/image-fallback-browser.js && node --check test/webp-fallback.mjs && node --check scripts/build-browser-check.mjs && node --check test/webp-fallback-browser.js && node --check test/image-fallback-boundary.mjs && node --check src/background.js && node --check test/background.mjs && node --check src/image-import.js && node --check test/image-import.mjs && node --check test/native-background.mjs && node --check src/background-import.js && node --check test/background-inheritance.mjs && node --check test/rich-table.mjs && node --check src/table-import.js && node --check test/rich-table-import.mjs && node --check test/rich-table-import-browser.js && node --check test/table-row-sizing.mjs && node --check test/table-styles.mjs && node --check test/table-styles-browser.js && node --check test/styled-table.mjs && node --check src/table-cell-import.js && node --check test/styled-table-import.mjs && node --check test/styled-table-import-browser.js && node --check src/table-border-import.js && node --check test/table-border-styles.mjs && node --check src/card-provenance.js && node --check test/content-cards.mjs && node --check src/chart-workbook.js && node --check test/chart-colors.mjs && node --check test/chart-workbook.mjs && node --check test/single-column-chart.mjs && node --check test/native-chart-colors.mjs && node --check src/heading-provenance.js && node --check test/accepted-text.mjs && node --check test/heading-provenance.mjs && node --check src/text-provenance.js && node --check test/plain-whitespace.mjs && node --check src/timeline-provenance.js && node --check test/shared-timeline.mjs && node --check test/shared-furniture.mjs && node --check src/furniture-provenance.js && node --check src/document-provenance.js && node --check test/document-provenance.mjs && node --check test/furniture-provenance.mjs && node --check src/furniture-fields.js && node --check test/furniture-fields.mjs && node --check test/furniture-wrapped-date.mjs && node --check test/native-picture.mjs && node --check test/compare-native-picture.mjs && node --check test/compare-native-picture-edit.mjs && node --check test/compare-native-picture-edit-controls.mjs && node --check test/native-font-embed-audit.mjs && node --check test/native-font-embed-controls.mjs && node --check test/native-font-embed-fixture-source.mjs && node --check test/native-font-edit-fixture.mjs && node --check test/native-mixed-edit-audit.mjs && node --check test/native-mixed-edit-controls.mjs && node --check test/master-bullet-font.mjs && node --check test/native-font-inventory-audit.mjs && node --check test/native-font-inventory-controls.mjs && node --check test/default-font-scheme.mjs && node --check test/export-chosen-fonts.mjs && node --check test/background-fills.mjs && node --check src/typeface-inventory.js && node --check src/package-fonts.js && node --check test/typeface-inventory.mjs && node --check src/script-fonts.js && node --check test/script-fonts.mjs && node --check test/powershell-scan.mjs && node --check test/powershell-scan-probes.mjs && node --check test/socials-furniture.mjs && node --check src/media-provenance.js && node --check test/layout-parity.mjs && node --check test/cover-centering.mjs && node --check test/media-placeholder.mjs && node --check test/image-placeholder.mjs && node --check test/list-shape-names.mjs && node --check test/layout-intent.mjs && node --check test/chart-cache-import.mjs && node --check test/zip-date.mjs && node --check test/native-text-whitespace.mjs && node --check test/native-underline.mjs && node --check src/native-text-style.js && node --check src/font-weights.js && node --check test/font-weight-import.mjs && node --check src/body-text-import.js && node --check test/native-body-rich-text.mjs && node --check test/native-body-rich-text-browser.js && node --check test/native-quote-import-contract.mjs && node --check test/native-quote-import-controls.mjs && node --check test/native-quote.mjs && node --check test/determinism.mjs && node --check test/determinism-fixtures.mjs && node --check test/table-frame-furniture-order.mjs && node --check test/shape-border-widths.mjs",
41
- "test": "npm run build && node test/chart-types.mjs && node test/chartex.mjs && node test/zip-date.mjs && node test/background-fills.mjs && npm run test:furniture && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/watermark.mjs && node test/media-dedupe.mjs && node test/document-provenance.mjs && node test/layout-intent.mjs && node test/master-bullet-font.mjs && node test/default-font-scheme.mjs && node test/script-fonts.mjs && node test/export-chosen-fonts.mjs && node test/typeface-inventory.mjs && node test/color-ref-export.mjs && node test/theme-colors.mjs && node test/table-theme-colors.mjs && node test/plain-whitespace.mjs && node test/native-text-whitespace.mjs && node test/native-body-rich-text.mjs && node test/native-underline.mjs && node test/font-weight-import.mjs && node test/design-alignment.mjs && node test/accepted-text.mjs && node test/heading-provenance.mjs && node test/native-quote-import-controls.mjs && node test/chart-colors.mjs && node test/content-cards.mjs && node test/chart-workbook.mjs && node test/single-column-chart.mjs && node --test test/chart-cache-import.mjs && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table && node test/content-layout.mjs && node test/layout-parity.mjs && node test/cover-centering.mjs && node test/table-frame-furniture-order.mjs && node test/media-placeholder.mjs && node test/shape-border-widths.mjs && node test/image-placeholder.mjs && node test/list-shape-names.mjs && node test/shared-quote.mjs && npm run test:timeline && npm run test:determinism",
40
+ "typecheck": "node --check src/index.js && node --check src/chart-types.js && node --check src/chartex.js && node --check test/chartex.mjs && node --check test/chart-types.mjs && node --check test/design-alignment.mjs && node --check src/color-ref.js && node --check src/theme-colors.js && node --check test/theme-colors.mjs && node --check test/table-theme-colors.mjs && node --check scripts/build.mjs && node --check scripts/validate-package.mjs && node --check test/smoke.mjs && node --check src/image-geometry.js && node --check src/slide-image-provenance.js && node --check test/slide-image.mjs && node --check test/slide-image-treatments.mjs && node --check src/watermark-provenance.js && node --check src/media-dedupe.js && node --check test/media-dedupe.mjs && node --check test/watermark.mjs && node --check test/image-fit.mjs && node --check test/image-orientation.mjs && node --check test/table-layout.mjs && node --check test/table-import.mjs && node --check test/object-ids.mjs && node --check test/export-corpus.mjs && node --check test/image-media-type.mjs && node --check src/image-fallback-node.js && node --check src/image-fallback-browser.js && node --check test/webp-fallback.mjs && node --check scripts/build-browser-check.mjs && node --check test/webp-fallback-browser.js && node --check test/image-fallback-boundary.mjs && node --check src/background.js && node --check test/background.mjs && node --check src/image-import.js && node --check test/image-import.mjs && node --check test/native-background.mjs && node --check src/background-import.js && node --check test/background-inheritance.mjs && node --check test/rich-table.mjs && node --check src/table-import.js && node --check test/rich-table-import.mjs && node --check test/rich-table-import-browser.js && node --check test/table-row-sizing.mjs && node --check test/table-styles.mjs && node --check test/table-styles-browser.js && node --check test/styled-table.mjs && node --check src/table-cell-import.js && node --check test/styled-table-import.mjs && node --check test/styled-table-import-browser.js && node --check src/table-border-import.js && node --check test/table-border-styles.mjs && node --check src/card-provenance.js && node --check test/content-cards.mjs && node --check src/chart-workbook.js && node --check test/chart-colors.mjs && node --check test/chart-workbook.mjs && node --check test/single-column-chart.mjs && node --check test/native-chart-colors.mjs && node --check src/heading-provenance.js && node --check test/accepted-text.mjs && node --check test/heading-provenance.mjs && node --check src/text-provenance.js && node --check test/plain-whitespace.mjs && node --check src/timeline-provenance.js && node --check src/quote-provenance.js && node --check test/quote-provenance.mjs && node --check test/shared-timeline.mjs && node --check test/shared-furniture.mjs && node --check src/furniture-provenance.js && node --check src/document-provenance.js && node --check test/document-provenance.mjs && node --check test/furniture-provenance.mjs && node --check src/furniture-fields.js && node --check test/furniture-fields.mjs && node --check test/furniture-wrapped-date.mjs && node --check test/native-picture.mjs && node --check test/compare-native-picture.mjs && node --check test/compare-native-picture-edit.mjs && node --check test/compare-native-picture-edit-controls.mjs && node --check test/native-font-embed-audit.mjs && node --check test/native-font-embed-controls.mjs && node --check test/native-font-embed-fixture-source.mjs && node --check test/native-font-edit-fixture.mjs && node --check test/native-mixed-edit-audit.mjs && node --check test/native-mixed-edit-controls.mjs && node --check test/master-bullet-font.mjs && node --check test/native-font-inventory-audit.mjs && node --check test/native-font-inventory-controls.mjs && node --check test/default-font-scheme.mjs && node --check test/export-chosen-fonts.mjs && node --check test/background-fills.mjs && node --check src/typeface-inventory.js && node --check src/package-fonts.js && node --check test/typeface-inventory.mjs && node --check src/script-fonts.js && node --check test/script-fonts.mjs && node --check test/theme-script-slots.mjs && node --check test/powershell-scan.mjs && node --check test/powershell-scan-probes.mjs && node --check test/socials-furniture.mjs && node --check src/media-provenance.js && node --check test/layout-parity.mjs && node --check test/cover-centering.mjs && node --check test/media-placeholder.mjs && node --check test/image-placeholder.mjs && node --check test/list-shape-names.mjs && node --check test/layout-intent.mjs && node --check test/chart-cache-import.mjs && node --check test/zip-date.mjs && node --check test/native-text-whitespace.mjs && node --check test/native-underline.mjs && node --check src/native-text-style.js && node --check src/font-weights.js && node --check test/font-weight-import.mjs && node --check src/body-text-import.js && node --check test/native-body-rich-text.mjs && node --check test/native-body-rich-text-browser.js && node --check test/native-quote-import-contract.mjs && node --check test/native-quote-import-controls.mjs && node --check test/native-quote.mjs && node --check test/determinism.mjs && node --check test/determinism-fixtures.mjs && node --check test/table-frame-furniture-order.mjs && node --check test/shape-border-widths.mjs && node --check test/gallery-layout-alignment.mjs",
41
+ "test": "npm run build && node test/chart-types.mjs && node test/chartex.mjs && node test/zip-date.mjs && node test/background-fills.mjs && npm run test:furniture && node test/slide-image.mjs && node test/slide-image-treatments.mjs && node test/watermark.mjs && node test/media-dedupe.mjs && node test/document-provenance.mjs && node test/layout-intent.mjs && node test/master-bullet-font.mjs && node test/default-font-scheme.mjs && node test/script-fonts.mjs && node test/theme-script-slots.mjs && node test/export-chosen-fonts.mjs && node test/typeface-inventory.mjs && node test/color-ref-export.mjs && node test/theme-colors.mjs && node test/table-theme-colors.mjs && node test/plain-whitespace.mjs && node test/native-text-whitespace.mjs && node test/native-body-rich-text.mjs && node test/native-underline.mjs && node test/font-weight-import.mjs && node test/design-alignment.mjs && node test/accepted-text.mjs && node test/heading-provenance.mjs && node test/native-quote-import-controls.mjs && node test/chart-colors.mjs && node test/content-cards.mjs && node test/chart-workbook.mjs && node test/single-column-chart.mjs && node --test test/chart-cache-import.mjs && node test/dependency-boundary.mjs && npm run build:browser-check && npm run test:styled-table && node test/content-layout.mjs && node test/layout-parity.mjs && node test/gallery-layout-alignment.mjs && node test/cover-centering.mjs && node test/table-frame-furniture-order.mjs && node test/media-placeholder.mjs && node test/shape-border-widths.mjs && node test/image-placeholder.mjs && node test/list-shape-names.mjs && node test/shared-quote.mjs && node test/quote-provenance.mjs && npm run test:timeline && npm run test:determinism",
42
42
  "validate": "node scripts/validate-package.mjs",
43
43
  "test:packed": "node test/packed-install.mjs",
44
44
  "test:compare-published": "node test/compare-published.mjs",
@@ -60,7 +60,7 @@
60
60
  "test:determinism": "node test/determinism.mjs"
61
61
  },
62
62
  "dependencies": {
63
- "@openpresentation/opf": "^0.11.2",
63
+ "@openpresentation/opf": "^0.11.3",
64
64
  "fast-xml-parser": "^5.8.0",
65
65
  "fflate": "^0.8.3",
66
66
  "jszip": "3.10.2",