@openpresentation/opf-pptx 0.13.1 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DEPENDENCY-NOTES.md +1 -1
- package/README.md +34 -16
- package/dist/annotation-export.js +1 -1
- package/dist/annotation-provenance.js +4 -0
- package/dist/background.js +2 -3
- package/dist/body-text-import.js +16 -4
- package/dist/chart-alt.js +54 -0
- package/dist/chart-data.js +29 -54
- package/dist/chart-highlight.js +113 -0
- package/dist/chart-options.js +46 -24
- package/dist/chart-types.js +59 -58
- package/dist/code-provenance.js +20 -4
- package/dist/color-ref.js +12 -6
- package/dist/data-provenance.js +18 -14
- package/dist/document-provenance.js +20 -8
- package/dist/format-check.js +10 -0
- package/dist/furniture-provenance.js +30 -3
- package/dist/heading-provenance.js +10 -3
- package/dist/image-fallback-node.js +4 -1
- package/dist/index.d.ts +32 -25
- package/dist/index.js +481 -235
- package/dist/media-provenance.js +3 -3
- package/dist/metric-provenance.js +3 -3
- package/dist/native-text-style.js +2 -0
- package/dist/package-fonts.js +2 -2
- package/dist/quote-provenance.js +70 -26
- package/dist/rich-heading.js +119 -0
- package/dist/run-colors.js +3 -0
- package/dist/script-fonts.js +81 -54
- package/dist/theme-colors.js +1 -1
- package/dist/timeline-provenance.js +17 -6
- package/dist/typeface-inventory.js +7 -7
- package/dist/watermark-provenance.js +65 -3
- package/package.json +5 -5
package/dist/data-provenance.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {XMLParser} from 'fast-xml-parser';
|
|
2
|
-
import {
|
|
2
|
+
import {checkFormat, isValidFormat} from './format-check.js';
|
|
3
3
|
import {decodeTextTag, encodeTextTag} from './code-provenance.js';
|
|
4
|
-
import {chartUsesDataFields, isDatasetRef,
|
|
4
|
+
import {chartUsesDataFields, isDatasetRef, fromExcelNumberFormat, resolveTableData, tableCellDisplayValue, tableUsesDataFields} from './chart-data.js';
|
|
5
5
|
|
|
6
6
|
// RR-54: chart and table data survive a PPTX round trip (core docs/chart-table-data.md).
|
|
7
7
|
//
|
|
@@ -13,11 +13,13 @@ import {chartUsesDataFields, isDatasetRef, numberFormatFromExcel, resolveTableDa
|
|
|
13
13
|
//
|
|
14
14
|
// OPF_DATASETS_V1 on p:presentation (beside OPF_DOCUMENT_V1): the whole `datasets` map, unused datasets included.
|
|
15
15
|
// OPF_DATA_V1 on each chart or table graphic frame that uses one of the fields above:
|
|
16
|
-
// chart {v: 1, kind: 'chart', data, mapping?, dataset?: hash, evidence: [hash]} `data` is the authored
|
|
16
|
+
// chart {v: 1, kind: 'chart', data, mapping?, highlight?, dataset?: hash, evidence: [hash]} `data` is the authored
|
|
17
17
|
// chart.data (a dataset reference, or inline columns with DataColumn objects and `source`); `evidence`
|
|
18
18
|
// hashes the native caches (series names, categories, values and format codes) of the chart part(s) the frame
|
|
19
19
|
// shows (the chartex part and its classic fallback); `dataset` hashes the referenced dataset, so a slide
|
|
20
|
-
// pasted into a deck whose dataset of the same id holds other data keeps its values inline.
|
|
20
|
+
// pasted into a deck whose dataset of the same id holds other data keeps its values inline. `highlight` (FA-14) is
|
|
21
|
+
// the authored chart.highlight: the native chart carries only the accent and muted colours it produced, which no
|
|
22
|
+
// exact inference turns back into series and category names, and a chart with only a highlight records its inline `data`.
|
|
21
23
|
// table {v: 1, kind: 'table', table} a dataset-backed table, as authored
|
|
22
24
|
// {v: 1, kind: 'table', headers: [[column, header, text]], cells: [[row, column, value, format, text]]}
|
|
23
25
|
// an inline table: the DataColumn or formatted
|
|
@@ -79,13 +81,13 @@ const canonicalValue = text => NUMBER.test(text.trim()) && Number.isFinite(Numbe
|
|
|
79
81
|
// A General format code is left out: on a plain save PowerPoint writes <c:formatCode>General</c:formatCode> into every
|
|
80
82
|
// cache that has none (PptxGenJS writes none on pie and doughnut series), so General and no code are the same evidence.
|
|
81
83
|
const generalCode = code => /^\s*general\s*$/i.test(code);
|
|
82
|
-
// A format code is hashed in core's canonical form (
|
|
84
|
+
// A format code is hashed in core's canonical form (fromExcelNumberFormat), because a plain PowerPoint save re-spells codes
|
|
83
85
|
// without changing what they show: `$#,##0.0` becomes `\$#,##0.0` and `#,##0 "units"` becomes `#,##0\ "units"` (native
|
|
84
86
|
// check opf#385). Core reads quoted and backslash-escaped literals and literal-safe characters to the same NumberFormat, so
|
|
85
87
|
// both spellings hash equal, while a real change (another number of decimals, another unit) still changes the hash. A code
|
|
86
88
|
// core cannot map is hashed as written; the `opf:` prefix keeps a mapped form apart from a verbatim code of the same text.
|
|
87
89
|
const formatEvidence = code => {
|
|
88
|
-
const format =
|
|
90
|
+
const format = fromExcelNumberFormat(code);
|
|
89
91
|
return typeof format === 'string' ? `format:opf:${format}` : `format:${code}`;
|
|
90
92
|
};
|
|
91
93
|
function hash(text) {
|
|
@@ -119,9 +121,9 @@ const datasetHash = (datasets, id) => own(datasets, id) ? hash(canonical(dataset
|
|
|
119
121
|
|
|
120
122
|
/** The record of a chart that uses an RR-54 data field, or undefined (the frame then carries no tag). */
|
|
121
123
|
export function chartDataRecord(chart, datasets) {
|
|
122
|
-
if (!chartUsesDataFields(chart)) return undefined;
|
|
124
|
+
if (!chartUsesDataFields(chart) && !object(chart?.highlight)) return undefined;
|
|
123
125
|
const dataset = isDatasetRef(chart.data) ? datasetHash(datasets, chart.data.dataset) : undefined;
|
|
124
|
-
return {v: 1, kind: 'chart', data: clone(chart.data), ...(chart.mapping !== undefined ? {mapping: clone(chart.mapping)} : {}), ...(dataset ? {dataset} : {})};
|
|
126
|
+
return {v: 1, kind: 'chart', data: clone(chart.data), ...(chart.mapping !== undefined ? {mapping: clone(chart.mapping)} : {}), ...(object(chart.highlight) ? {highlight: clone(chart.highlight)} : {}), ...(dataset ? {dataset} : {})};
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
/** The record of a table that is dataset-backed or formats numbers, or undefined. `layout` is the exported table layout. */
|
|
@@ -281,8 +283,9 @@ export function readDatasetsTag(entries, presentationRoot, relationships, report
|
|
|
281
283
|
const datasets = readTags(entries, presentationRoot?.['p:custDataLst'], relationships, DATASETS_TAG);
|
|
282
284
|
if (datasets === undefined) return undefined;
|
|
283
285
|
if (!object(datasets) || !Object.keys(datasets).length) throw Error('Invalid datasets record.');
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
+
const report = checkFormat({slides: [{title: 'Datasets'}], datasets});
|
|
287
|
+
const [first] = report.findings.filter(finding => finding.severity === 'error');
|
|
288
|
+
if (!report.valid) throw Error(`The stored datasets do not validate (${first?.path ?? ''} ${first?.message ?? ''}).`);
|
|
286
289
|
return datasets;
|
|
287
290
|
} catch (error) {
|
|
288
291
|
report?.({code: 'invalid-data-provenance', path: 'datasets', message: `${error instanceof Error ? error.message : String(error)} The datasets recorded at export were not restored.`});
|
|
@@ -297,6 +300,7 @@ export function readDataTag(entries, frame, relationships) {
|
|
|
297
300
|
if (!object(record) || record.v !== 1 || !['chart', 'table'].includes(record.kind)) throw Error('Unsupported data provenance version.');
|
|
298
301
|
if (record.kind === 'chart' && (!object(record.data) || !Array.isArray(record.evidence) || !record.evidence.every(item => typeof item === 'string'))) throw Error('Invalid chart data record.');
|
|
299
302
|
if (record.kind === 'chart' && record.dataset !== undefined && typeof record.dataset !== 'string') throw Error('Invalid chart data record.');
|
|
303
|
+
if (record.kind === 'chart' && record.highlight !== undefined && !object(record.highlight)) throw Error('Invalid chart data record.');
|
|
300
304
|
if (record.kind === 'table' && !(object(record.table) || (Array.isArray(record.headers) && Array.isArray(record.cells)))) throw Error('Invalid table data record.');
|
|
301
305
|
// Inline table entries are [column, header, text] and [row, column, value, format, text]; their indices are array
|
|
302
306
|
// indices (non-negative integers), never a key such as "__proto__" or "length".
|
|
@@ -309,7 +313,7 @@ export function readDataTag(entries, frame, relationships) {
|
|
|
309
313
|
return record;
|
|
310
314
|
}
|
|
311
315
|
|
|
312
|
-
const validWith = (payload, datasets) =>
|
|
316
|
+
const validWith = (payload, datasets) => isValidFormat({slides: [{title: 'Data', ...payload}], ...(datasets ? {datasets} : {})});
|
|
313
317
|
|
|
314
318
|
/**
|
|
315
319
|
* The authored chart data while the native caches are unchanged: the imported chart (type and options) with the recorded
|
|
@@ -317,7 +321,7 @@ const validWith = (payload, datasets) => validatePresentation({slides: [{title:
|
|
|
317
321
|
*/
|
|
318
322
|
export function restoreChartData(chart, record, evidence, {datasets, report}) {
|
|
319
323
|
if (!record.evidence.includes(evidence)) {
|
|
320
|
-
report({code: 'chart-data-provenance-changed', message: 'The chart\'s cached data or number formats changed after export (for example in Edit Data), so the dataset reference, column mapping, column formats and data source recorded at export were not restored; the chart imports the values and formats it now shows.'});
|
|
324
|
+
report({code: 'chart-data-provenance-changed', message: 'The chart\'s cached data or number formats changed after export (for example in Edit Data), so the dataset reference, column mapping, column formats and data source recorded at export were not restored; the chart imports the values and formats it now shows.' + (record.highlight ? ' The chart highlight (series and categories named at export) was not restored either: its names refer to the data as exported.' : '')});
|
|
321
325
|
return chart;
|
|
322
326
|
}
|
|
323
327
|
if (isDatasetRef(record.data) && !own(datasets, record.data.dataset)) {
|
|
@@ -328,8 +332,8 @@ export function restoreChartData(chart, record, evidence, {datasets, report}) {
|
|
|
328
332
|
report({code: 'chart-dataset-unavailable', message: `The chart referenced dataset '${record.data.dataset}', but this package's dataset of that id holds other data than at export (a slide pasted from another deck); the chart imports its values inline.`});
|
|
329
333
|
return chart;
|
|
330
334
|
}
|
|
331
|
-
const {mapping: _mapping, ...rest} = chart;
|
|
332
|
-
const restored = {...rest, data: clone(record.data), ...(record.mapping !== undefined ? {mapping: clone(record.mapping)} : {})};
|
|
335
|
+
const {mapping: _mapping, highlight: _highlight, ...rest} = chart;
|
|
336
|
+
const restored = {...rest, data: clone(record.data), ...(record.mapping !== undefined ? {mapping: clone(record.mapping)} : {}), ...(record.highlight !== undefined ? {highlight: clone(record.highlight)} : {})};
|
|
333
337
|
if (!validWith({chart: restored}, isDatasetRef(record.data) ? datasets : undefined)) {
|
|
334
338
|
report({code: 'invalid-data-provenance', message: 'The chart data recorded at export does not form a valid chart; the chart imports the values it shows.'});
|
|
335
339
|
return chart;
|
|
@@ -2,8 +2,8 @@ import {XMLParser} from 'fast-xml-parser';
|
|
|
2
2
|
import {decodeTextTag, encodeTextTag} from './code-provenance.js';
|
|
3
3
|
import {contentTopology, rebuildContent, validateTopology, listLineBreaks, validateListBreaks} from './content-topology.js';
|
|
4
4
|
import {runColorRecord, validateRunColors} from './run-colors.js';
|
|
5
|
-
|
|
6
|
-
import
|
|
5
|
+
import {catalogs as bundledCatalogs, validateCatalogRecord} from '@openpresentation/opf';
|
|
6
|
+
import {resolveSocialProfile} from '@openpresentation/opf/composition';
|
|
7
7
|
|
|
8
8
|
// FF-32: document and slide references survive a PPTX round trip.
|
|
9
9
|
//
|
|
@@ -61,11 +61,11 @@ const own = (value, key) => object(value) && Object.hasOwn(value, key) && value[
|
|
|
61
61
|
// still shows exactly what the stored authored value formats to, keep the
|
|
62
62
|
// authored form (a handle stays a handle); an edited line keeps its new URL.
|
|
63
63
|
function authoredSocials(stored, observed, records) {
|
|
64
|
-
if (!object(stored) || !object(observed)
|
|
64
|
+
if (!object(stored) || !object(observed)) return observed;
|
|
65
65
|
return Object.fromEntries(Object.entries(observed).map(([platform, value]) => {
|
|
66
66
|
const authored = stored[platform];
|
|
67
67
|
if (typeof authored !== 'string') return [platform, value];
|
|
68
|
-
const profile =
|
|
68
|
+
const profile = resolveSocialProfile(platform, authored, records, 'organization');
|
|
69
69
|
const shown = profile.href && !/^[a-z][a-z0-9+.-]*:/i.test(profile.text) ? `https://${profile.text}` : profile.text;
|
|
70
70
|
return [platform, shown === value ? authored : value];
|
|
71
71
|
}));
|
|
@@ -370,7 +370,7 @@ const layoutRecords = catalogs => array(Array.isArray(catalogs?.layouts) ? catal
|
|
|
370
370
|
function validLayoutRecord(value) {
|
|
371
371
|
// Inline catalogs already identify the kind. Supply only an omitted
|
|
372
372
|
// standalone identifier for validation; never change the authored record.
|
|
373
|
-
try { return object(value) &&
|
|
373
|
+
try { return object(value) && validateCatalogRecord('layouts', {$schema: 'https://openpresentation.org/schema/opf-layout/v1', ...value}).valid; }
|
|
374
374
|
catch { return false; }
|
|
375
375
|
}
|
|
376
376
|
|
|
@@ -872,7 +872,7 @@ export function slideListBreaks(entries, path, root, rels) {
|
|
|
872
872
|
* `nativeSections` is the package's native section list as one name (or
|
|
873
873
|
* undefined) per slide, or null when the package has no list.
|
|
874
874
|
*/
|
|
875
|
-
export function restoreDocumentProvenance(imported, {entries, presentationRoot, presentationRels, slides, organizationConflict = false, socialPlatformRecords, nativeSections = null}, report) {
|
|
875
|
+
export function restoreDocumentProvenance(imported, {entries, presentationRoot, presentationRels, slides, organizationConflict = false, speakerConflict = false, socialPlatformRecords, nativeSections = null}, report) {
|
|
876
876
|
const invalid = message => report({code: 'invalid-document-provenance', path: '', message: `${message} Ordinary import keeps the values observed in the PPTX.`});
|
|
877
877
|
let document;
|
|
878
878
|
try {
|
|
@@ -1109,6 +1109,10 @@ export function restoreDocumentProvenance(imported, {entries, presentationRoot,
|
|
|
1109
1109
|
report({code: 'metadata-reference-changed', path: 'organization', message: 'Slides now show different organization names in their furniture, so the stored organization was not restored; each slide keeps its visible text.'});
|
|
1110
1110
|
continue;
|
|
1111
1111
|
}
|
|
1112
|
+
if (key === 'speaker' && speakerConflict) {
|
|
1113
|
+
report({code: 'metadata-reference-changed', path: 'speaker', message: 'Slides now show different speaker names in their furniture, so the stored speaker was not restored; each slide keeps its visible text.'});
|
|
1114
|
+
continue;
|
|
1115
|
+
}
|
|
1112
1116
|
if (key === 'author') {
|
|
1113
1117
|
// Native evidence: the stored authored form only stands for the creator it was joined into.
|
|
1114
1118
|
if (joinAuthors(imported.author) === joinAuthors(metadata.author)) group('author', [set(['author'], clone(metadata.author))]);
|
|
@@ -1120,7 +1124,7 @@ export function restoreDocumentProvenance(imported, {entries, presentationRoot,
|
|
|
1120
1124
|
if (key === 'organization' && object(imported.organization)) {
|
|
1121
1125
|
const observed = imported.organization;
|
|
1122
1126
|
// Same order as export: inline records, then the document source, host catalogs and bundled records.
|
|
1123
|
-
const hostRecords = typeof socialPlatformRecords === 'function' ? socialPlatformRecords(document.catalogs) :
|
|
1127
|
+
const hostRecords = typeof socialPlatformRecords === 'function' ? socialPlatformRecords(document.catalogs) : bundledCatalogs.socialPlatforms;
|
|
1124
1128
|
const records = [...array(document.catalogs?.socialPlatforms?.records ?? document.catalogs?.socialPlatforms), ...hostRecords].filter(object);
|
|
1125
1129
|
const merge = (stored, current) => object(current.socials) && object(stored?.socials) ? {...current, socials: authoredSocials(stored.socials, current.socials, records)} : current;
|
|
1126
1130
|
const list = array(value);
|
|
@@ -1129,6 +1133,14 @@ export function restoreDocumentProvenance(imported, {entries, presentationRoot,
|
|
|
1129
1133
|
else if (Array.isArray(value)) value[index] = {...list[index], ...merge(list[index], clone(observed))};
|
|
1130
1134
|
else value = {...value, ...merge(value, clone(observed))};
|
|
1131
1135
|
}
|
|
1136
|
+
if (key === 'speaker' && object(imported.speaker)) {
|
|
1137
|
+
// The furniture speaker line (name and title) is native content and wins over the stored speaker with the same id.
|
|
1138
|
+
const observed = imported.speaker, list = array(value), index = list.findIndex(item => object(item) && item.id === observed.id);
|
|
1139
|
+
const merged = stored => { const {title: _title, ...rest} = stored; return {...rest, ...clone(observed)}; };
|
|
1140
|
+
if (index < 0) value = clone(observed);
|
|
1141
|
+
else if (Array.isArray(value)) value[index] = merged(list[index]);
|
|
1142
|
+
else value = merged(value);
|
|
1143
|
+
}
|
|
1132
1144
|
group(key, [set([key], value)]);
|
|
1133
1145
|
}
|
|
1134
1146
|
|
|
@@ -1167,7 +1179,7 @@ export function restoreDocumentProvenance(imported, {entries, presentationRoot,
|
|
|
1167
1179
|
* that the imported document references.
|
|
1168
1180
|
*/
|
|
1169
1181
|
function layoutIntent(documentRecords, hasDocument, slideRecords, entries, group, report, rejectedDocumentIds) {
|
|
1170
|
-
const bundled = id => array(
|
|
1182
|
+
const bundled = id => array(bundledCatalogs.layouts).find(item => item?.id === id);
|
|
1171
1183
|
const owns = slideRecords.map(entry => {
|
|
1172
1184
|
if (!entry) return null;
|
|
1173
1185
|
const {record, native: observed} = entry;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// The one question the provenance and import code asks core: does this value satisfy the OPF format (the schema, and the
|
|
2
|
+
// checks that make a deck structurally usable)? It is core's cheap `format` category: no layout is built, so it is safe on
|
|
3
|
+
// every import and export path.
|
|
4
|
+
import {validate} from '@openpresentation/opf';
|
|
5
|
+
|
|
6
|
+
/** Core's `format` report for a deck or a fragment (`findings`, `valid`). */
|
|
7
|
+
export const checkFormat = value => validate(value, {only: ['format']});
|
|
8
|
+
|
|
9
|
+
/** True when the value has no `format` error. */
|
|
10
|
+
export const isValidFormat = value => checkFormat(value).valid;
|
|
@@ -15,7 +15,7 @@ const enc = new TextEncoder(), dec = new TextDecoder('utf-8', {fatal: true});
|
|
|
15
15
|
const parser = new XMLParser({ignoreAttributes: false, attributeNamePrefix: '', parseTagValue: false, trimValues: false});
|
|
16
16
|
const array = value => value === undefined ? [] : Array.isArray(value) ? value : [value];
|
|
17
17
|
const kinds = ['header', 'footer'], zones = ['left', 'center', 'right'];
|
|
18
|
-
const fields = ['text', 'image', 'organization', 'socials', 'section', 'slideNumber', 'date'];
|
|
18
|
+
const fields = ['text', 'image', 'organization', 'speaker', 'socials', 'section', 'slideNumber', 'date'];
|
|
19
19
|
const settings = ['slideNumberFormat', 'dateFormat'];
|
|
20
20
|
const dateFormatForField = Object.fromEntries(Object.entries(NATIVE_DATE_FIELDS).map(([format, type]) => [type, format]));
|
|
21
21
|
// Platform keys follow the Socials schema's propertyNames pattern exactly, so
|
|
@@ -97,6 +97,7 @@ export function furnitureManifest(presentation, slide, layout, slideIndex, stati
|
|
|
97
97
|
if (!Object.keys(definitions).length) return null;
|
|
98
98
|
const organizations = array(presentation.organization);
|
|
99
99
|
const organization = organizations.find(item => item.role === 'primary') ?? organizations[0];
|
|
100
|
+
const speaker = array(presentation.speaker)[0];
|
|
100
101
|
// Generated deck logos (logo: true) are listed beside the topology, never inside it: the part list and the field
|
|
101
102
|
// definitions are validated strictly by every released importer (0.11.6 and earlier reject an unknown field there),
|
|
102
103
|
// so they keep describing only what those importers know. `drawn` is false when no logo resolved at export.
|
|
@@ -109,9 +110,12 @@ export function furnitureManifest(presentation, slide, layout, slideIndex, stati
|
|
|
109
110
|
}
|
|
110
111
|
return {v: 1, role: 'slide', group: String(slideIndex), definitions, ...(Object.keys(formats).length ? {formats} : {}),
|
|
111
112
|
...(layout.parts.some(part => part.field === 'organization' || part.field === 'socials') ? {organizationId: organization?.id} : {}),
|
|
113
|
+
...(layout.parts.some(part => part.field === 'speaker') ? {speakerId: speaker?.id} : {}),
|
|
112
114
|
parts: layout.parts.flatMap((part, index) => part.field === 'logo' ? [] : [{kind: part.kind, zone: part.zone, field: part.field,
|
|
113
115
|
type: part.type, count: part.type === 'image' ? 1 : part.fit.lines.length,
|
|
114
116
|
...(part.field === 'socials' ? {socials: socialLines(part)} : {}),
|
|
117
|
+
// The speaker part is one line, "Name, Title" or "Name": the name's length lets import split it again (lengths, never words).
|
|
118
|
+
...(part.field === 'speaker' ? {nameLength: speaker.name.length} : {}),
|
|
115
119
|
...(staticDates.has(index) ? {staticDate: staticDates.get(index)} : {}),
|
|
116
120
|
// RR-11: this part is a native PowerPoint placeholder (dt, ftr or sldNum); deleting it in PowerPoint's dialog is then intent.
|
|
117
121
|
...(natives.has(index) ? {ph: natives.get(index)} : {})}]),
|
|
@@ -209,8 +213,12 @@ function validateManifest(manifest) {
|
|
|
209
213
|
&& part.socials.every(line => object(line) && platformId.test(line.platform) && schemes.includes(line.scheme))
|
|
210
214
|
&& new Set(part.socials.map(line => line.platform)).size === part.socials.length, 'Invalid social profile lines.');
|
|
211
215
|
else check(part.socials === undefined, 'Unexpected social profile lines.');
|
|
216
|
+
if (part.field === 'speaker') check(Number.isSafeInteger(part.nameLength) && part.nameLength >= 1 && part.nameLength <= 100000, 'Invalid speaker name length.');
|
|
217
|
+
else check(part.nameLength === undefined, 'Unexpected speaker name length.');
|
|
212
218
|
expected.delete(key(part));
|
|
213
219
|
}
|
|
220
|
+
if (manifest.parts.some(part => part.field === 'speaker')) check(typeof manifest.speakerId === 'string' && /^[a-zA-Z0-9_-]+$/.test(manifest.speakerId), 'Invalid speaker identity.');
|
|
221
|
+
else check(manifest.speakerId === undefined, 'Unexpected speaker identity.');
|
|
214
222
|
if (manifest.parts.some(part => part.field === 'organization' || part.field === 'socials')) check(typeof manifest.organizationId === 'string' && /^[a-zA-Z0-9_-]+$/.test(manifest.organizationId), 'Invalid organization identity.');
|
|
215
223
|
}
|
|
216
224
|
|
|
@@ -269,7 +277,7 @@ function readSlide(context, entries, slideIndex, slideCount, report, taggedText)
|
|
|
269
277
|
if (value !== false) for (const zone of zones) if (definition.value[zone] !== undefined) {
|
|
270
278
|
value[zone] = Object.fromEntries(Object.entries(definition.value[zone]).filter(([, flag]) => flag === false));
|
|
271
279
|
}
|
|
272
|
-
const candidate = {scope: definition.scope, value, text: [], pictures: [], organizations: [], socials: [], sections: []};
|
|
280
|
+
const candidate = {scope: definition.scope, value, text: [], pictures: [], organizations: [], speakers: [], socials: [], sections: []};
|
|
273
281
|
// logo: true returns as its flag; the generated picture is consumed, never content or an image field.
|
|
274
282
|
for (const logo of (manifest.logos ?? []).filter(item => item.kind === kind)) {
|
|
275
283
|
const found = logoPictures.filter(item => item.data.v === 1 && item.data.group === manifest.group && item.data.furniture === kind && item.data.zone === logo.zone);
|
|
@@ -305,6 +313,7 @@ function readSlide(context, entries, slideIndex, slideCount, report, taggedText)
|
|
|
305
313
|
}
|
|
306
314
|
if (part.field === 'section') candidate.sections.push(text);
|
|
307
315
|
if (part.field === 'organization') candidate.organizations.push({id: manifest.organizationId, name: text});
|
|
316
|
+
if (part.field === 'speaker') candidate.speakers.push(importedSpeaker(manifest.speakerId, text, part.nameLength));
|
|
308
317
|
if (part.field === 'socials') {
|
|
309
318
|
const lines = text.split('\n');
|
|
310
319
|
check(lines.length === part.socials.length && lines.every(line => line.trim()), 'Current social profile lines no longer match their platforms.');
|
|
@@ -330,6 +339,14 @@ function readSlide(context, entries, slideIndex, slideCount, report, taggedText)
|
|
|
330
339
|
return candidates;
|
|
331
340
|
}
|
|
332
341
|
|
|
342
|
+
// The generated speaker line is "Name, Title" (or "Name"). The recorded name length splits it again; a line edited in
|
|
343
|
+
// PowerPoint that no longer has the separator at that position is the visible text as the name, with no title.
|
|
344
|
+
function importedSpeaker(id, text, nameLength) {
|
|
345
|
+
const rest = text.slice(nameLength);
|
|
346
|
+
if (text.length > nameLength && rest.startsWith(', ') && rest.length > 2) return {id, name: text.slice(0, nameLength), title: rest.slice(2)};
|
|
347
|
+
return {id, name: text};
|
|
348
|
+
}
|
|
349
|
+
|
|
333
350
|
// A current native date field wins. Full-mode static-wrap evidence can recover
|
|
334
351
|
// generated OPF intent without turning the existing PPTX text into a live field. A fixed
|
|
335
352
|
// date keeps its ISO value and pattern only when the current text round-trips.
|
|
@@ -368,6 +385,13 @@ export function importFurniture(contexts, entries, onDiagnostic) {
|
|
|
368
385
|
delete slide[kind]; report(index, `${kind}: Current organization metadata disagrees across repeated fields.`);
|
|
369
386
|
}
|
|
370
387
|
}
|
|
388
|
+
const speakers = candidates.flatMap(slide => Object.values(slide).flatMap(candidate => candidate.speakers));
|
|
389
|
+
const speakerConflict = speakers.some(item => !same(item, speakers[0]));
|
|
390
|
+
if (speakerConflict) {
|
|
391
|
+
for (const [index, slide] of candidates.entries()) for (const kind of kinds) if (slide[kind]?.speakers.length) {
|
|
392
|
+
delete slide[kind]; report(index, `${kind}: Current speaker metadata disagrees across repeated fields.`);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
371
395
|
for (const [index, slide] of candidates.entries()) {
|
|
372
396
|
const sections = Object.values(slide).flatMap(candidate => candidate.sections);
|
|
373
397
|
if (sections.some(section => section !== sections[0])) for (const kind of kinds) if (slide[kind]?.sections.length) {
|
|
@@ -398,7 +422,7 @@ export function importFurniture(contexts, entries, onDiagnostic) {
|
|
|
398
422
|
}
|
|
399
423
|
if (!accepted.length) return;
|
|
400
424
|
const merged = footerValue(accepted), indexes = accepted.map(item => item.index);
|
|
401
|
-
if (!existing) slide.footer = {scope: 'native', value: merged, text: indexes, pictures: [], organizations: [], socials: [], sections: []};
|
|
425
|
+
if (!existing) slide.footer = {scope: 'native', value: merged, text: indexes, pictures: [], organizations: [], speakers: [], socials: [], sections: []};
|
|
402
426
|
else {
|
|
403
427
|
if (existing.value === false) existing.value = {};
|
|
404
428
|
for (const zone of Object.keys(merged)) existing.value[zone] = {...existing.value[zone], ...merged[zone]};
|
|
@@ -417,6 +441,9 @@ export function importFurniture(contexts, entries, onDiagnostic) {
|
|
|
417
441
|
if (result.organization && validSocials.length) result.organization = {...result.organization, socials: validSocials[0].socials};
|
|
418
442
|
// Stored document metadata (FF-32) must not override disagreeing visible names.
|
|
419
443
|
if (organizationConflict) result.organizationConflict = true;
|
|
444
|
+
const validSpeakers = candidates.flatMap(slide => Object.values(slide).flatMap(candidate => candidate.speakers));
|
|
445
|
+
if (validSpeakers.length) result.speaker = validSpeakers[0];
|
|
446
|
+
if (speakerConflict) result.speakerConflict = true;
|
|
420
447
|
for (const kind of kinds) {
|
|
421
448
|
const inherited = candidates.map(slide => slide[kind]).filter(candidate => candidate?.scope === 'global');
|
|
422
449
|
if (inherited.length && candidates.every(slide => slide[kind]) && inherited.every(candidate => same(candidate.value, inherited[0].value))) {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {XMLParser} from 'fast-xml-parser';
|
|
2
2
|
import {attachTextTags,decodeTextTag} from './code-provenance.js';
|
|
3
3
|
import {sourceLineParagraphs} from './text-provenance.js';
|
|
4
|
+
import {cleanBase,headingValue,joinRichLines} from './rich-heading.js';
|
|
4
5
|
const TAG='OPF_HEADING_V1',REL='http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags';
|
|
5
6
|
const parser=new XMLParser({ignoreAttributes:false,attributeNamePrefix:'',parseTagValue:false,trimValues:false});
|
|
6
7
|
const decoder=new TextDecoder('utf-8',{fatal:true}),array=value=>value===undefined?[]:Array.isArray(value)?value:[value];
|
|
@@ -8,7 +9,8 @@ export const attachHeadingTags=(entries,records)=>attachTextTags(entries,records
|
|
|
8
9
|
|
|
9
10
|
// Tags carry only roles and line ordering. Current native text always wins;
|
|
10
11
|
// neither original source text nor old geometry can be restored from these tags.
|
|
11
|
-
|
|
12
|
+
// `readBody` (optional) is the native body reader: with it a group also returns `richText`, the heading as string | TextRun[] (FA-10).
|
|
13
|
+
export function importHeadingGroups(shapes,paragraphs,relationships,entries,report,readBody) {
|
|
12
14
|
const groups=new Map(),consumed=new Set(),items=[];
|
|
13
15
|
for(const [index,shape] of shapes.entries()) {
|
|
14
16
|
const tags=[];let unreadable=false;
|
|
@@ -37,8 +39,13 @@ export function importHeadingGroups(shapes,paragraphs,relationships,entries,repo
|
|
|
37
39
|
}
|
|
38
40
|
const restored=sourceLineParagraphs(ordered,paragraphs,{legacy:true});
|
|
39
41
|
for(const item of ordered)consumed.add(item.shape);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
// The same boundaries as the plain text: a new tag's separator, or a native line break between legacy-tagged lines.
|
|
43
|
+
const source=ordered.some(item=>item.data.boundary!==undefined||item.data.separator!==undefined);
|
|
44
|
+
// Only a rich export records the heading's own look (tag.base); a heading exported as a plain string stays one.
|
|
45
|
+
const base=cleanBase(ordered[0].data.base);
|
|
46
|
+
const parts=base?joinRichLines(ordered,readBody,(item,last)=>source?item.data.separator:last?'':'\n'):undefined;
|
|
47
|
+
items.push({field:first.field,shapes:ordered.map(item=>item.shape),paragraphs:restored,...(parts?{richText:headingValue(parts,base)}:{})});
|
|
48
|
+
report({code:'heading-import-reflow',message:'Complete tagged heading lines retain their roles, order and current native text. New boundary tags also retain authored whitespace and line endings; legacy tags retain native line breaks. Run formatting that differs between words is read back as TextRun[]; geometry is not reconstructed.'});
|
|
42
49
|
}catch(error){report({code:'invalid-heading-provenance',message:`${error.message} Ordinary import retains visible native text.`});}
|
|
43
50
|
return {consumed,items};
|
|
44
51
|
}
|
|
@@ -11,6 +11,9 @@ export async function webpToPng(bytes) {
|
|
|
11
11
|
// is deterministic (no system fonts, no network or file access for the SVG's own references). `text` selects the
|
|
12
12
|
// bundled fonts; an SVG with no text needs none. Rejects with code `svg-rasterizer-unavailable` when opf-render is
|
|
13
13
|
// not installed.
|
|
14
|
+
//
|
|
15
|
+
// This is the one place the exporter calls the renderer. opf-render's svgToPng(svg, {fonts, scale, background}) takes the
|
|
16
|
+
// fonts handle for the faces it may draw with: here only the bundled ones, never the system's.
|
|
14
17
|
export async function svgToPng(svg, {scale, text}) {
|
|
15
18
|
let render;
|
|
16
19
|
try {
|
|
@@ -20,7 +23,7 @@ export async function svgToPng(svg, {scale, text}) {
|
|
|
20
23
|
unavailable.code = 'svg-rasterizer-unavailable';
|
|
21
24
|
throw unavailable;
|
|
22
25
|
}
|
|
23
|
-
return new Uint8Array(await render(svg, {scale, background: 'rgba(0, 0, 0, 0)'
|
|
26
|
+
return new Uint8Array(await render(svg, {fonts: {useBundledFonts: text === true, loadSystemFonts: false}, scale, background: 'rgba(0, 0, 0, 0)'}));
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
// A local SVG file (a path source), read as bytes; PptxGenJS reads raster paths itself.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LayoutDiagnostic,
|
|
1
|
+
import type { Finding, FontSchemeDiagnostic, Fonts, LayoutDiagnostic, SlideContextReferenceDiagnostic } from "@openpresentation/opf";
|
|
2
2
|
export declare const packageName = "@openpresentation/opf-pptx";
|
|
3
3
|
|
|
4
4
|
export declare const releaseLane: Readonly<{
|
|
@@ -33,10 +33,9 @@ export interface ImageResolverContext {
|
|
|
33
33
|
path: string;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export interface FontSchemeDiagnostic { code: "unresolved-font-scheme"; path: string; message: string; id: string; fallback: string }
|
|
37
36
|
export interface MediaProvenanceDiagnostic { code: "media-provenance-omitted"; path: string; message: string }
|
|
38
37
|
/** The chart data cannot be plotted, so a placeholder frame stands in for the chart. */
|
|
39
|
-
export interface ChartDataUnplottableDiagnostic { code: "chart-data-unplottable"; path: string; message: string; reason: "
|
|
38
|
+
export interface ChartDataUnplottableDiagnostic { code: "chart-data-unplottable"; path: string; message: string; reason: "dataset-unknown" | "no-rows" | "no-columns" | "single-column-not-numeric" }
|
|
40
39
|
/** Content with no PowerPoint export (an empty table, an unsupported payload) is replaced by a plain-language placeholder frame. */
|
|
41
40
|
export interface ContentPlaceholderDiagnostic { code: "content-placeholder"; path: string; message: string; reason: "table-has-no-rows" | "unsupported-payload" }
|
|
42
41
|
/**
|
|
@@ -53,7 +52,6 @@ export interface UnresolvedAssetDiagnostic { code: "unresolved-asset"; path: str
|
|
|
53
52
|
export interface SvgSanitizedDiagnostic { code: "svg-sanitized"; path: string; message: string }
|
|
54
53
|
/** An SVG slide image with a duotone recolor or a non-rectangular shape exports as its PNG raster, not as a native SVG picture, so the effect applies as in the preview (PowerPoint applies opacity, grayscale and a border to an SVG picture, and those stay native). */
|
|
55
54
|
export interface SvgImageRasterizedDiagnostic { code: "svg-image-rasterized"; path: string; message: string }
|
|
56
|
-
export interface WatermarkNotExportedDiagnostic { code: "watermark-not-exported"; path: string; message: string }
|
|
57
55
|
/**
|
|
58
56
|
* The chart data was reshaped to export a native chart: a single value column was plotted against row numbers; a one-series construct
|
|
59
57
|
* (pie, doughnut, and with `chartex: "native"` treemap, histogram, pareto, waterfall, funnel, map) kept only its first series; by default
|
|
@@ -70,9 +68,9 @@ export interface ChartMappingAdaptedDiagnostic { code: "chart-mapping-adapted";
|
|
|
70
68
|
/** RR-54 (`full` provenance): a chart or table data record, or the datasets map (`path: "datasets"`), is over the 16 MiB tag limit and is not stored; the native values still export. */
|
|
71
69
|
export interface DataProvenanceOmittedDiagnostic { code: "data-provenance-omitted"; path: string; message: string }
|
|
72
70
|
|
|
73
|
-
/** A template variable was unfilled and its `example` was used (template export only). */
|
|
71
|
+
/** A template variable was unfilled and its `example` was used (template export only), or a built-in variable such as `{{speaker.name}}` has no source value in the document (`variable-builtin-missing`; it exports as nothing). */
|
|
74
72
|
export interface VariableExampleUsedDiagnostic {
|
|
75
|
-
code: "variable-example-used";
|
|
73
|
+
code: "variable-example-used" | "variable-builtin-missing";
|
|
76
74
|
/** JSON pointer of the variable's declaration. */
|
|
77
75
|
path: string;
|
|
78
76
|
message: string;
|
|
@@ -112,11 +110,17 @@ export interface ToPptxOptions {
|
|
|
112
110
|
* unfilled required variable throws `unfilled-variables`, and a value of the wrong kind throws `invalid-variables`.
|
|
113
111
|
*/
|
|
114
112
|
variables?: Record<string, unknown>;
|
|
115
|
-
|
|
113
|
+
/**
|
|
114
|
+
* The fonts handle: what the renderer's `loadFonts()` returns (`@openpresentation/opf-render/fonts-node`), or any object with a
|
|
115
|
+
* `textMeasurement` (core's `Fonts`). The exporter lays text out with `fonts.textMeasurement` and always names the chosen
|
|
116
|
+
* families, never a substitute face. Without it, layout uses core's portable estimate. This is the only way to pass a measurement:
|
|
117
|
+
* a top-level `textMeasurement` option is not read.
|
|
118
|
+
*/
|
|
119
|
+
fonts?: Fonts;
|
|
116
120
|
/** Match preview/pagination clearance around supplied vector text outlines; default 1. */
|
|
117
121
|
textRasterPadding?: number;
|
|
118
|
-
/** Layout diagnostics, `media-provenance-omitted` when video data cannot be stored, plus `unresolved-font-scheme` (once per reference path) when a font-scheme id matches no record and the default `aptos` scheme is used as the base. */
|
|
119
|
-
onDiagnostic?: (diagnostic: LayoutDiagnostic | FontSchemeDiagnostic | MediaProvenanceDiagnostic | ChartDataUnplottableDiagnostic | ChartDataAdaptedDiagnostic | ChartMapGeodataDiagnostic | ChartValueNotNumericDiagnostic | ChartMappingAdaptedDiagnostic | DataProvenanceOmittedDiagnostic | ContentPlaceholderDiagnostic | UnresolvedAssetDiagnostic |
|
|
122
|
+
/** Layout diagnostics, `media-provenance-omitted` when video data cannot be stored, plus `unresolved-font-scheme` (once per reference path) when a font-scheme id matches no record and the default `aptos` scheme is used as the base, and, from core's slide context, `unresolved-theme`, `unresolved-color-scheme` and `unresolved-layout` (once per reference path) when an id matches no record: the default theme (`minimal`) and colour scheme (`cool-horizon`) are used, and a slide whose layout is unknown, like one with no layout, is composed with no layout record. None of them refuses the export. */
|
|
123
|
+
onDiagnostic?: (diagnostic: LayoutDiagnostic | FontSchemeDiagnostic | SlideContextReferenceDiagnostic | MediaProvenanceDiagnostic | ChartDataUnplottableDiagnostic | ChartDataAdaptedDiagnostic | ChartMapGeodataDiagnostic | ChartValueNotNumericDiagnostic | ChartMappingAdaptedDiagnostic | DataProvenanceOmittedDiagnostic | ContentPlaceholderDiagnostic | UnresolvedAssetDiagnostic | SvgSanitizedDiagnostic | SvgImageRasterizedDiagnostic | VariableExampleUsedDiagnostic) => void;
|
|
120
124
|
baseDir?: string;
|
|
121
125
|
compressionLevel?: number;
|
|
122
126
|
imageResolver?: (src: string, context: ImageResolverContext) => ImageResolverResult | Promise<ImageResolverResult | null | undefined> | null | undefined;
|
|
@@ -148,7 +152,7 @@ export interface ToPptxOptions {
|
|
|
148
152
|
date?: string;
|
|
149
153
|
/** Host-supplied catalog records, as in opf-render. Currently consulted for socialPlatforms (generated socials furniture). */
|
|
150
154
|
catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
151
|
-
/** Records for
|
|
155
|
+
/** Records for presentation `catalogs.<kind>.source` URLs (a single source or each entry of an ordered search path), as in opf-render. Currently consulted for socialPlatforms. */
|
|
152
156
|
catalogSources?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
153
157
|
}
|
|
154
158
|
|
|
@@ -159,12 +163,12 @@ export interface FromPptxOptions {
|
|
|
159
163
|
schema?: string;
|
|
160
164
|
/** The export's host catalog records (as in ToPptxOptions). Currently used to recognize unedited socials lines, so authored handles return. */
|
|
161
165
|
catalogs?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
162
|
-
/** The export's records for
|
|
166
|
+
/** The export's records for presentation `catalogs.<kind>.source` URLs (as in ToPptxOptions). Currently used for socialPlatforms. */
|
|
163
167
|
catalogSources?: Record<string, { records?: unknown[] } | unknown[]>;
|
|
164
168
|
/**
|
|
165
169
|
* Opt in to raw per-shape signals (see PptxSignals). `true` uses the default limits; an object lowers or raises them.
|
|
166
|
-
* With it, `fromPptx` resolves to `{
|
|
167
|
-
* to the
|
|
170
|
+
* With it, `fromPptx` resolves to `{presentation, signals}`; without it (the default, also `false`/`null`) it resolves
|
|
171
|
+
* to the presentation alone. The presentation is the same either way.
|
|
168
172
|
*/
|
|
169
173
|
signals?: boolean | PptxSignalOptions | null;
|
|
170
174
|
}
|
|
@@ -172,7 +176,10 @@ export interface FromPptxOptions {
|
|
|
172
176
|
export declare class OPFPptxError extends Error {
|
|
173
177
|
readonly code: string;
|
|
174
178
|
readonly details: Record<string, unknown>;
|
|
175
|
-
|
|
179
|
+
/** The `error` findings of the `format` check that refused the input or the imported presentation (`invalid-opf`, `invalid-import-opf`), or the variable errors (`unfilled-variables`, `invalid-variables`). */
|
|
180
|
+
readonly findings?: Finding[];
|
|
181
|
+
/** The layout diagnostics behind a `layout-overflow` error. */
|
|
182
|
+
readonly diagnostics?: LayoutDiagnostic[];
|
|
176
183
|
readonly path?: string;
|
|
177
184
|
constructor(code: string, message: string, details?: Record<string, unknown>);
|
|
178
185
|
}
|
|
@@ -195,7 +202,7 @@ export declare function fromPptx(input: Uint8Array | ArrayBuffer, options?: From
|
|
|
195
202
|
|
|
196
203
|
/** Bounds on the signals. Each is an integer from 1 up to the cap in parentheses; the defaults are DEFAULT_SIGNAL_LIMITS. */
|
|
197
204
|
export interface PptxSignalOptions {
|
|
198
|
-
/** Slides reported (5000). The
|
|
205
|
+
/** Slides reported (5000). The presentation always has every slide. Default 300. */
|
|
199
206
|
maxSlides?: number;
|
|
200
207
|
/** Shapes reported per slide, groups and their members each counting one (5000). Default 300. */
|
|
201
208
|
maxShapesPerSlide?: number;
|
|
@@ -221,8 +228,8 @@ export declare const SIGNALS_VERSION: 1;
|
|
|
221
228
|
export declare function isMonospaceFamily(family: string | undefined): boolean;
|
|
222
229
|
|
|
223
230
|
export interface PptxImportWithSignals {
|
|
224
|
-
/** The same
|
|
225
|
-
|
|
231
|
+
/** The same presentation `fromPptx` returns without the option. */
|
|
232
|
+
presentation: Record<string, unknown>;
|
|
226
233
|
signals: PptxSignals;
|
|
227
234
|
}
|
|
228
235
|
|
|
@@ -252,7 +259,7 @@ export interface PptxSignals {
|
|
|
252
259
|
}
|
|
253
260
|
|
|
254
261
|
export interface PptxSlideSignals {
|
|
255
|
-
/** Slide position, the same index as `
|
|
262
|
+
/** Slide position, the same index as `presentation.slides`. */
|
|
256
263
|
index: number;
|
|
257
264
|
/** Package part, for example "ppt/slides/slide1.xml". */
|
|
258
265
|
part: string;
|
|
@@ -379,7 +386,7 @@ export interface PptxOutlineSignal extends PptxColor {
|
|
|
379
386
|
tailArrow?: string;
|
|
380
387
|
}
|
|
381
388
|
|
|
382
|
-
/** Where a shape went in the
|
|
389
|
+
/** Where a shape went in the presentation `fromPptx` returned. */
|
|
383
390
|
export interface PptxOpfLink {
|
|
384
391
|
/**
|
|
385
392
|
* "block" fed a block (`path`, `blockType`); "title", "subtitle" and "tag" fed that slide field. Other roles name a shape the
|
|
@@ -387,7 +394,7 @@ export interface PptxOpfLink {
|
|
|
387
394
|
* and the members of an OPF-tagged group ("code", "metric", "quote", "timeline", "media", "card-frame").
|
|
388
395
|
*/
|
|
389
396
|
role: string;
|
|
390
|
-
/** Path in the
|
|
397
|
+
/** Path in the presentation, for example "slides.2.blocks.1", "slides.0.title", or "slides.1.left" after a tagged round trip. */
|
|
391
398
|
path?: string;
|
|
392
399
|
blockType?: string;
|
|
393
400
|
}
|
|
@@ -462,9 +469,9 @@ export interface TypefaceInventory {
|
|
|
462
469
|
fontsUsed: string[] | null;
|
|
463
470
|
}
|
|
464
471
|
|
|
465
|
-
export interface
|
|
466
|
-
/** Family names the
|
|
467
|
-
|
|
472
|
+
export interface CheckTypefacesOptions {
|
|
473
|
+
/** Family names the presentation chose (heading, body, code, run fonts). Required. */
|
|
474
|
+
families: string[];
|
|
468
475
|
/** Chosen families that are monospace; their pitchFamily must be fixed pitch, and only theirs. */
|
|
469
476
|
monospace?: string[];
|
|
470
477
|
/** Allow empty theme ea/cs slots and references to them (FF-05). Default true. */
|
|
@@ -504,11 +511,11 @@ export interface TypefaceViolation {
|
|
|
504
511
|
|
|
505
512
|
export declare const THEME_SCRIPT_SUPPLEMENTS: Readonly<Record<"major" | "minor", Readonly<Record<string, string>>>>;
|
|
506
513
|
|
|
507
|
-
export declare function
|
|
514
|
+
export declare function inventoryTypefaces(input: Uint8Array | ArrayBuffer | Record<string, Uint8Array>, options?: {nested?: boolean}): TypefaceInventory;
|
|
508
515
|
|
|
509
516
|
export declare function packageFontsUsed(inventory: TypefaceInventory): string[];
|
|
510
517
|
|
|
511
|
-
export declare function
|
|
518
|
+
export declare function checkTypefaces(input: Uint8Array | ArrayBuffer | Record<string, Uint8Array>, options: CheckTypefacesOptions): {
|
|
512
519
|
ok: boolean;
|
|
513
520
|
violations: TypefaceViolation[];
|
|
514
521
|
inventory: TypefaceInventory;
|