@openpresentation/opf-pptx 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -26
- package/dist/background-import.js +33 -17
- package/dist/background.js +28 -25
- package/dist/chart-types.js +7 -22
- package/dist/data-provenance.js +68 -11
- package/dist/document-provenance.js +235 -132
- package/dist/{chart-alt.js → frame-alt.js} +4 -4
- package/dist/image-fallback-browser.js +5 -0
- package/dist/image-fallback-node.js +20 -2
- package/dist/image-provenance.js +273 -0
- package/dist/index.d.ts +39 -20
- package/dist/index.js +333 -196
- package/dist/script-fonts.js +41 -60
- package/dist/theme-colors.js +15 -10
- package/dist/typeface-inventory.js +1 -1
- package/package.json +15 -4
- package/dist/slide-image-provenance.js +0 -177
package/dist/data-provenance.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {XMLParser} from 'fast-xml-parser';
|
|
2
2
|
import {checkFormat, isValidFormat} from './format-check.js';
|
|
3
3
|
import {decodeTextTag, encodeTextTag} from './code-provenance.js';
|
|
4
|
-
import {chartUsesDataFields, isDatasetRef, fromExcelNumberFormat, resolveTableData, tableCellDisplayValue
|
|
4
|
+
import {chartUsesDataFields, isDatasetRef, fromExcelNumberFormat, resolveTableData, tableCellDisplayValue} 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
|
//
|
|
@@ -27,7 +27,9 @@ import {chartUsesDataFields, isDatasetRef, fromExcelNumberFormat, resolveTableDa
|
|
|
27
27
|
//
|
|
28
28
|
// Import restores a record only while its native evidence is unchanged: a chart while its caches hash the same (an edit in
|
|
29
29
|
// PowerPoint's Edit Data changes them), a dataset table while every native cell still shows the dataset's display text,
|
|
30
|
-
// and an inline table cell by cell while
|
|
30
|
+
// and an inline table cell by cell while its native text, formatting and grid evidence is unchanged (RR-59).
|
|
31
|
+
// RR-59 adds `authored`, per-cell `evidence` and an `environment` hash of theme/table style parts; the legacy headers/cells
|
|
32
|
+
// fields remain readable by older importers. Exact authored style absence is restored only with all of this evidence. Otherwise the native values stay (with
|
|
31
33
|
// the format codes core maps back, src/chart-data.js) and a diagnostic names what was not restored. Tags are untrusted
|
|
32
34
|
// input: they are size-limited (16 MiB, the limit of the document tag), depth- and shape-checked, validated as OPF, and
|
|
33
35
|
// never executed; a record that fails a check, or that a restore cannot use, is reported and the native values stay.
|
|
@@ -113,6 +115,32 @@ export function chartEvidence(xml) {
|
|
|
113
115
|
return hash(parts.join('\u0000'));
|
|
114
116
|
}
|
|
115
117
|
|
|
118
|
+
/** Native cell text, formatting and table geometry, canonicalized independently of XML attribute order. */
|
|
119
|
+
export function tableCellEvidence(table, relationships = new Map()) {
|
|
120
|
+
const related = value => {
|
|
121
|
+
const ids = new Set(), stack = [value];
|
|
122
|
+
while (stack.length) {
|
|
123
|
+
const item = stack.pop();
|
|
124
|
+
if (!item || typeof item !== 'object') continue;
|
|
125
|
+
for (const [key, child] of Object.entries(item)) {
|
|
126
|
+
if (/^r:(id|embed|link)$/.test(key) && typeof child === 'string') ids.add(child);
|
|
127
|
+
else if (child && typeof child === 'object') stack.push(child);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return ids.size ? {relationships: Object.fromEntries([...ids].sort().map(id => {
|
|
131
|
+
const link = relationships.get(id);
|
|
132
|
+
return [id, link ? {type: link.type, target: link.target, targetMode: link.targetMode} : null];
|
|
133
|
+
}))} : {};
|
|
134
|
+
};
|
|
135
|
+
const properties = {properties: table?.['a:tblPr'], grid: table?.['a:tblGrid']};
|
|
136
|
+
return array(table?.['a:tr']).map(row => array(row?.['a:tc']).map(cell => hash(canonical({properties, row: Object.fromEntries(Object.entries(row).filter(([key]) => key !== 'a:tc')), cell, ...related({properties, cell})}))));
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Theme and table-style edits can change effective cell formatting without changing the cell XML. */
|
|
140
|
+
export function tableEnvironmentEvidence(entries) {
|
|
141
|
+
return hash(canonical(Object.fromEntries(Object.keys(entries).filter(path => /^ppt\/theme\/[^/]+\.xml$/.test(path) || path === 'ppt/tableStyles.xml').sort().map(path => [path, parser.parse(dec.decode(entries[path]))]))));
|
|
142
|
+
}
|
|
143
|
+
|
|
116
144
|
// ---------------------------------------------------------------------------
|
|
117
145
|
// Export
|
|
118
146
|
|
|
@@ -126,19 +154,16 @@ export function chartDataRecord(chart, datasets) {
|
|
|
126
154
|
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} : {})};
|
|
127
155
|
}
|
|
128
156
|
|
|
129
|
-
/**
|
|
157
|
+
/** Full table provenance: authored inline cells (with legacy numeric recovery fields), or a dataset reference. */
|
|
130
158
|
export function tableDataRecord(table, layout) {
|
|
131
|
-
if (!tableUsesDataFields(table)) return undefined;
|
|
132
159
|
if (isDatasetRef(table)) return {v: 1, kind: 'table', table: clone(table)};
|
|
133
160
|
const hasHeaders = Array.isArray(table.columns) && table.columns.length > 0;
|
|
134
161
|
const shown = (row, column) => cellText(layout?.rows?.[row]?.cells?.find(cell => cell.column === column)?.value);
|
|
135
162
|
// A header's format: a DataColumn's or a header StyledTableCell's (both are the column format).
|
|
136
163
|
const formatOf = value => object(value) && typeof value.format === 'string' ? value.format : undefined;
|
|
137
164
|
const headers = [], cells = [];
|
|
138
|
-
const columnFormats = [];
|
|
139
165
|
(hasHeaders ? table.columns : []).forEach((header, column) => {
|
|
140
166
|
const format = formatOf(header);
|
|
141
|
-
columnFormats[column] = format;
|
|
142
167
|
const dataColumn = object(header) && typeof header.name === 'string' && !own(header, 'value');
|
|
143
168
|
if (dataColumn || format !== undefined) headers.push([column, clone(header), shown(0, column)]);
|
|
144
169
|
});
|
|
@@ -147,11 +172,11 @@ export function tableDataRecord(table, layout) {
|
|
|
147
172
|
row.forEach((cell, column) => {
|
|
148
173
|
const value = own(cell, 'value') ? cell.value : cell;
|
|
149
174
|
const format = own(cell, 'value') && typeof cell.format === 'string' ? cell.format : undefined;
|
|
150
|
-
if (typeof value !== 'number'
|
|
175
|
+
if (typeof value !== 'number') return;
|
|
151
176
|
cells.push([rowIndex, column, value, format ?? null, shown(rowIndex + (hasHeaders ? 1 : 0), column)]);
|
|
152
177
|
});
|
|
153
178
|
});
|
|
154
|
-
return {v: 1, kind: 'table', headers, cells};
|
|
179
|
+
return {v: 1, kind: 'table', headers, cells, authored: clone(table)};
|
|
155
180
|
}
|
|
156
181
|
|
|
157
182
|
function relationshipsXml(entries, path) {
|
|
@@ -205,7 +230,9 @@ export function attachDataProvenance(entries, {records, paths = new Map(), datas
|
|
|
205
230
|
if (seen.has(name)) throw Error(`Duplicate generated frame name ${name}.`);
|
|
206
231
|
seen.add(name);
|
|
207
232
|
const record = records.get(name);
|
|
208
|
-
const
|
|
233
|
+
const native = record.kind === 'table' ? parser.parse(frames.find(frame => frame.match(/<p:cNvPr\b[^>]*\bname="([^"]+)"/)?.[1] === name))?.['p:graphicFrame']?.['a:graphic']?.['a:graphicData']?.['a:tbl'] : undefined;
|
|
234
|
+
const value = record.kind === 'chart' ? {...record, evidence: evidence.get(name) ?? []} : {...record, evidence: tableCellEvidence(native, relationships), environment: tableEnvironmentEvidence(entries)};
|
|
235
|
+
if (!fits(value, paths.get(name) ?? name)) {records.delete(name); seen.delete(name); continue;}
|
|
209
236
|
const part = `ppt/tags/opfData${++count}.xml`;
|
|
210
237
|
if (entries[part]) throw Error('Colliding data tag part.');
|
|
211
238
|
entries[part] = tagPart(DATA_TAG, value);
|
|
@@ -310,6 +337,9 @@ export function readDataTag(entries, frame, relationships) {
|
|
|
310
337
|
&& (entry[3] === null || typeof entry[3] === 'string') && typeof entry[4] === 'string';
|
|
311
338
|
if (!record.headers.every(header) || !record.cells.every(cell)) throw Error('Invalid table data record.');
|
|
312
339
|
}
|
|
340
|
+
if (record.kind === 'table' && record.authored !== undefined && !object(record.authored)) throw Error('Invalid authored table record.');
|
|
341
|
+
if (record.kind === 'table' && record.evidence !== undefined && (!Array.isArray(record.evidence) || !record.evidence.every(row => Array.isArray(row) && row.every(cell => typeof cell === 'string')) || typeof record.environment !== 'string')) throw Error('Invalid table evidence record.');
|
|
342
|
+
if (record.kind === 'table' && record.authored !== undefined && record.evidence === undefined) throw Error('Authored table record has no native evidence.');
|
|
313
343
|
return record;
|
|
314
344
|
}
|
|
315
345
|
|
|
@@ -341,12 +371,17 @@ export function restoreChartData(chart, record, evidence, {datasets, report}) {
|
|
|
341
371
|
return restored;
|
|
342
372
|
}
|
|
343
373
|
|
|
344
|
-
/** The authored table data where the native
|
|
345
|
-
export function restoreTableData(table, record, {datasets, report}) {
|
|
374
|
+
/** The authored table data where the native evidence is unchanged; edited native cells otherwise. */
|
|
375
|
+
export function restoreTableData(table, record, {datasets, report, evidence, environment}) {
|
|
346
376
|
if (!object(table)) return table;
|
|
347
377
|
const columns = Array.isArray(table.columns) ? table.columns : undefined;
|
|
348
378
|
const rows = Array.isArray(table.rows) ? table.rows : [];
|
|
379
|
+
const sameCell = (row, column) => record.environment === environment && record.evidence?.[row]?.[column] === evidence?.[row]?.[column];
|
|
349
380
|
if (record.table) {
|
|
381
|
+
if (record.evidence && (record.environment !== environment || canonical(record.evidence) !== canonical(evidence))) {
|
|
382
|
+
report({code: 'table-data-provenance-changed', message: 'The table text, formatting or geometry changed after export; the dataset reference was not restored and native cells are retained.'});
|
|
383
|
+
return table;
|
|
384
|
+
}
|
|
350
385
|
const id = record.table.dataset;
|
|
351
386
|
if (!own(datasets, id)) {
|
|
352
387
|
report({code: 'table-dataset-unavailable', message: `The table referenced dataset '${id}', which this package's datasets record does not hold; the table imports its cells inline.`});
|
|
@@ -363,6 +398,28 @@ export function restoreTableData(table, record, {datasets, report}) {
|
|
|
363
398
|
}
|
|
364
399
|
return clone(record.table);
|
|
365
400
|
}
|
|
401
|
+
if (record.authored) {
|
|
402
|
+
if (!validWith({table: record.authored})) {
|
|
403
|
+
report({code: 'invalid-data-provenance', message: 'The authored table recorded at export is invalid; native cells are retained.'});
|
|
404
|
+
return table;
|
|
405
|
+
}
|
|
406
|
+
const authored = record.authored, headerOffset = authored.columns?.length ? 1 : 0;
|
|
407
|
+
const expected = [...(headerOffset ? [authored.columns] : []), ...authored.rows];
|
|
408
|
+
if (record.environment === environment && canonical(record.evidence) === canonical(evidence)) return clone(authored);
|
|
409
|
+
const next = {...table, ...(columns ? {columns: [...columns]} : {}), rows: rows.map(row => [...row])};
|
|
410
|
+
let changed = 0;
|
|
411
|
+
expected.forEach((row, r) => row.forEach((cell, c) => {
|
|
412
|
+
const target = r < headerOffset ? next.columns : next.rows[r - headerOffset];
|
|
413
|
+
if (target && c < target.length && sameCell(r, c)) target[c] = clone(cell);
|
|
414
|
+
else changed++;
|
|
415
|
+
}));
|
|
416
|
+
if (!validWith({table: next})) {
|
|
417
|
+
report({code: 'table-data-provenance-changed', message: 'The authored cells no longer form a valid table with the native grid; native cells are retained.'});
|
|
418
|
+
return table;
|
|
419
|
+
}
|
|
420
|
+
if (changed) report({code: 'table-data-provenance-changed', message: `${changed} authored table ${changed === 1 ? 'cell was' : 'cells were'} not restored because ${changed === 1 ? 'its' : 'their'} native text, formatting or geometry changed; native edits are retained.`});
|
|
421
|
+
return next;
|
|
422
|
+
}
|
|
366
423
|
const next = {...table, ...(columns ? {columns: [...columns]} : {}), rows: rows.map(row => Array.isArray(row) ? [...row] : row)};
|
|
367
424
|
let changed = 0;
|
|
368
425
|
for (const entry of record.headers) {
|