@openpresentation/opf-pptx 0.11.3 → 0.11.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPENDENCY-NOTES.md +1 -1
- package/README.md +12 -6
- package/dist/index.js +33 -11
- package/dist/quote-provenance.js +89 -0
- package/dist/script-fonts.js +11 -11
- package/dist/slide-image-provenance.js +7 -1
- package/dist/typeface-inventory.js +11 -1
- package/package.json +4 -4
package/DEPENDENCY-NOTES.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Coordinated `@openpresentation/opf` pin
|
|
4
4
|
|
|
5
|
-
PPTX 0.11.
|
|
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.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).
|
|
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
|
|
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
|
-
-
|
|
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.
|
|
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);
|
|
@@ -1196,6 +1205,12 @@ function nativeColor(reference, hex, context, fallback) {
|
|
|
1196
1205
|
return (link && context.linkSentinels?.[link]) ?? normalizeHex(hex);
|
|
1197
1206
|
}
|
|
1198
1207
|
|
|
1208
|
+
// FF-59: the slide tag is the eyebrow label. It is written in the deck primary color, the scheme accent1 slot the
|
|
1209
|
+
// preview draws it from: a:schemeClr accent1 where the deck theme holds that exact color, the literal otherwise.
|
|
1210
|
+
function tagColor(context) {
|
|
1211
|
+
return nativeColor('primary', context.colors.accent, context);
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1199
1214
|
// FF-24c: two literals no document color uses stand in for hlink and folHlink
|
|
1200
1215
|
// (see nativeColor). When the document uses every candidate, those colors stay literal.
|
|
1201
1216
|
const LINK_SENTINELS = [['FE01A0', 'FE01A1'], ['FE02B0', 'FE02B1'], ['FE03C0', 'FE03C1']];
|
|
@@ -1216,7 +1231,7 @@ function writeLinkSentinels(xml, sentinels) {
|
|
|
1216
1231
|
|
|
1217
1232
|
// PptxGenJS color option: a scheme value it can emit (tx1, bg2, ...) or RRGGBB.
|
|
1218
1233
|
function pptxColor(value) {
|
|
1219
|
-
return /^(?:tx[12]|bg[12])$/.test(value) ? value : normalizeHex(value);
|
|
1234
|
+
return /^(?:tx[12]|bg[12]|accent[1-6])$/.test(value) ? value : normalizeHex(value);
|
|
1220
1235
|
}
|
|
1221
1236
|
|
|
1222
1237
|
// Deck-level theme background for the slide master, so slides added in
|
|
@@ -1353,7 +1368,7 @@ async function addSlide(pptx, presentation, opfSlide, slideIndex, context, optio
|
|
|
1353
1368
|
fill:paint(surface),line:{...paint(itemContext.colorScheme.accent5??`#${itemContext.colors.border}`),width:.75},objectName:`OPF card ${item.path}`});
|
|
1354
1369
|
}
|
|
1355
1370
|
if(['text','title','subtitle','tag'].includes(item.field)&&(item.text?.placement||item.text?.sourceLines)&&!item.text.richLines) {
|
|
1356
|
-
addMeasuredPayloadText(slide,item.value,item.box,itemContext,options,{path:item.path,fit:item.text,textStyle:item.textStyle,sourceText:item.field==='text'&&!!item.text.sourceLines,align:alignmentFor(item),diagnosticsHandled:true,heading:['title','subtitle','tag'].includes(item.field)?item.field:undefined,color:item.field==='tag'?itemContext
|
|
1371
|
+
addMeasuredPayloadText(slide,item.value,item.box,itemContext,options,{path:item.path,fit:item.text,textStyle:item.textStyle,sourceText:item.field==='text'&&!!item.text.sourceLines,align:alignmentFor(item),diagnosticsHandled:true,heading:['title','subtitle','tag'].includes(item.field)?item.field:undefined,color:item.field==='tag'?tagColor(itemContext):itemContext.textColor});
|
|
1357
1372
|
} else if (["title", "subtitle", "tag"].includes(item.field)) {
|
|
1358
1373
|
// Estimated-font headings use one native text box, which still needs a
|
|
1359
1374
|
// role tag. Role recovery must not depend on outline measurement support.
|
|
@@ -1362,7 +1377,7 @@ async function addSlide(pptx, presentation, opfSlide, slideIndex, context, optio
|
|
|
1362
1377
|
slide.addText(item.text.lines.join("\n"), {
|
|
1363
1378
|
...textBoxOptions(region, itemContext, item.text.fontSize * 0.75),
|
|
1364
1379
|
...nativeFontOptions(item.textStyle),
|
|
1365
|
-
color: item.field === "tag" ? itemContext
|
|
1380
|
+
color: item.field === "tag" ? tagColor(itemContext) : itemContext.textColor,
|
|
1366
1381
|
align: alignmentFor(item),
|
|
1367
1382
|
objectName,
|
|
1368
1383
|
breakLine: false
|
|
@@ -1461,7 +1476,7 @@ async function addPayload(slide, presentation, payload, region, path, context, o
|
|
|
1461
1476
|
addMetricPayload(slide, payload.metric,metricLayout,context,path);
|
|
1462
1477
|
break;
|
|
1463
1478
|
case "quote":
|
|
1464
|
-
addQuotePayload(slide, quoteLayout, context, options, path);
|
|
1479
|
+
addQuotePayload(slide, payload.quote, quoteLayout, context, options, path);
|
|
1465
1480
|
break;
|
|
1466
1481
|
case "timeline":
|
|
1467
1482
|
addTimelinePayload(slide, payload.timeline, timelineLayout, context, options, path);
|
|
@@ -1641,7 +1656,9 @@ async function addSlideImage(slide, presentation, image, slideIndex, context, op
|
|
|
1641
1656
|
: image.recolor?.type === 'duotone' ? { type: 'duotone', dark: paint(image.recolor.dark, '000000'), light: paint(image.recolor.light, 'FFFFFF') } : null,
|
|
1642
1657
|
overlay: image.overlay ? { ...paint(image.overlay.color, context.colors.text), opacity: image.overlay.opacity, box: image.overlay.box, shape: image.overlay.shape } : null,
|
|
1643
1658
|
};
|
|
1644
|
-
|
|
1659
|
+
// FF-53: a root `image` with the slide image's source is the slide image (core `replacesContent`); the
|
|
1660
|
+
// manifest records that so an unchanged picture imports back as both design.slideImage and slide.image.
|
|
1661
|
+
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
1662
|
slide.addImage({ ...resolved, objectName, ...box, altText: image.alt ?? assetAlt(image.value, presentation) });
|
|
1646
1663
|
// The overlay scrim is a separate native shape directly above the picture.
|
|
1647
1664
|
if (effects.overlay) {
|
|
@@ -1832,12 +1849,13 @@ function addMeasuredPayloadText(slide, text, box, context, options, config) {
|
|
|
1832
1849
|
for (const [index, line] of fit.lines.entries()) {
|
|
1833
1850
|
const link = config.links?.[sourceLineIndex];
|
|
1834
1851
|
if (fit.sourceLines?.[index]?.boundary === 'hard') sourceLineIndex += 1;
|
|
1835
|
-
if (!line&&!config.heading&&!config.sourceText&&!config.timeline&&!config.keepEmpty) continue;
|
|
1852
|
+
if (!line&&!config.heading&&!config.sourceText&&!config.timeline&&!config.quote&&!config.keepEmpty) continue;
|
|
1836
1853
|
const alignment=fit.placement?.alignment??config.align??context.contentAlignment??'left',placed=fit.placement?.lines[index],factor=alignment==='right'?1:alignment==='center'?.5:0;
|
|
1837
1854
|
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;
|
|
1855
|
+
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
1856
|
if(config.heading)context.headingTags.set(objectName,{v:1,group:config.path,field:config.heading,line:index,count:fit.lines.length,...boundary});
|
|
1840
1857
|
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}:{})});
|
|
1858
|
+
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
1859
|
else if(config.furniture){
|
|
1842
1860
|
context.furnitureTags.set(objectName,{v:1,role:'text',...config.furniture,line:index,count:fit.lines.length,...boundary});
|
|
1843
1861
|
if(config.liveFields?.[index]?.length)context.furnitureFields.set(objectName,{text:line,fields:config.liveFields[index]});
|
|
@@ -1956,13 +1974,16 @@ function addMetricPayload(slide,value,layout,context,path) {
|
|
|
1956
1974
|
}
|
|
1957
1975
|
}
|
|
1958
1976
|
|
|
1959
|
-
function addQuotePayload(slide, layout, context, options, path) {
|
|
1977
|
+
function addQuotePayload(slide, value, layout, context, options, path) {
|
|
1960
1978
|
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
|
-
|
|
1979
|
+
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});
|
|
1980
|
+
// Every line shape is tagged (OPF_QUOTE_V1) so an unchanged export re-imports as a quote payload (FF-57).
|
|
1981
|
+
const group=String(context.quoteTags.size),anchor=quoteManifest(value,layout);
|
|
1982
|
+
for (const [index,part] of layout.parts.entries()) {
|
|
1963
1983
|
addMeasuredPayloadText(slide,part.text,part.box,context,options,{
|
|
1964
1984
|
path:part.path,fit:part.fit,textStyle:part.style,diagnosticsHandled:true,
|
|
1965
1985
|
color:part.role==='footer'?context.mutedColor:context.textColor,
|
|
1986
|
+
quote:{group,part:index,anchor},
|
|
1966
1987
|
});
|
|
1967
1988
|
}
|
|
1968
1989
|
}
|
|
@@ -2522,6 +2543,7 @@ async function normalizePptxZip(raw, context) {
|
|
|
2522
2543
|
attachHeadingTags(entries,context.headingTags);
|
|
2523
2544
|
attachPlainTextTags(entries,context.plainTextTags);
|
|
2524
2545
|
attachTimelineTags(entries,context.timelineTags);
|
|
2546
|
+
attachQuoteTags(entries,context.quoteTags);
|
|
2525
2547
|
attachFurnitureFields(entries,context.furnitureFields);
|
|
2526
2548
|
attachFurnitureTags(entries,context.furnitureTags,context.furnitureManifests);
|
|
2527
2549
|
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
|
+
}
|
package/dist/script-fonts.js
CHANGED
|
@@ -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
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
* script
|
|
71
|
-
*
|
|
72
|
-
*
|
|
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.
|
|
83
|
-
const face =
|
|
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
|
-
|
|
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-
|
|
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
|
+
"version": "0.11.5",
|
|
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/tag-colour.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/tag-colour.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.
|
|
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",
|