@openpresentation/opf-pptx 0.20.3 → 0.22.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 +65 -3
- package/dist/annotation-provenance.js +11 -2
- package/dist/body-text-import.js +64 -7
- package/dist/chart-scheme-colors.js +47 -0
- package/dist/chart-workbook.js +7 -5
- package/dist/code-provenance.js +34 -4
- package/dist/errors.js +11 -0
- package/dist/furniture-fields.js +14 -1
- package/dist/furniture-provenance.js +13 -5
- package/dist/heading-provenance.js +3 -3
- package/dist/image-provenance.js +65 -7
- package/dist/index.d.ts +37 -1
- package/dist/index.js +145 -70
- package/dist/input-safety.js +115 -0
- package/dist/metric-provenance.js +4 -4
- package/dist/native-body.js +147 -0
- package/dist/native-footer-classification.js +104 -0
- package/dist/native-furniture.js +52 -17
- package/dist/native-text-style.js +7 -2
- package/dist/quote-provenance.js +3 -3
- package/dist/svg-image.js +9 -2
- package/dist/template-layouts.js +30 -14
- package/dist/text-provenance.js +3 -3
- package/dist/timeline-provenance.js +3 -3
- package/dist/typeface-inventory.js +5 -5
- package/package.json +4 -4
package/dist/template-layouts.js
CHANGED
|
@@ -78,18 +78,27 @@ export function placeholderType(region) {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/** Which slide objects a placeholder of each type takes (binding a `none` region's lone object). */
|
|
81
|
-
const BINDS = {title: ['title'], ctrTitle: ['title'], pic: ['picture'], chart: ['chart'], tbl: ['table'], obj: ['picture', 'chart', 'table']};
|
|
81
|
+
const BINDS = {title: ['title'], ctrTitle: ['title'], body: ['body'], pic: ['picture'], chart: ['chart'], tbl: ['table'], obj: ['picture', 'chart', 'table']};
|
|
82
82
|
export const bindsObject = (type, object) => (BINDS[type] ?? []).includes(object);
|
|
83
83
|
|
|
84
84
|
/** The placeholder index of each region: its kind's base plus its position among the regions of that kind (REGION_IDX_BASE). */
|
|
85
|
-
export function regionIndexes(record) {
|
|
85
|
+
export function regionIndexes(record, nativeBodyRegion) {
|
|
86
86
|
const regions = new Map(layoutTemplate(record).regions.map(region => [region.name, region]));
|
|
87
87
|
const counts = {};
|
|
88
|
-
|
|
88
|
+
const indexes = Object.fromEntries(Object.keys(record?.regions ?? {}).filter(name => regions.has(name)).map(name => {
|
|
89
89
|
const type = placeholderType(regions.get(name));
|
|
90
90
|
counts[type] = (counts[type] ?? -1) + 1;
|
|
91
91
|
return [name, REGION_IDX_BASE[type] + counts[type]];
|
|
92
92
|
}));
|
|
93
|
+
// A native variant changes only its selected slot. Other object kinds retain
|
|
94
|
+
// their original indexes, so Change Layout cannot move a neighbouring picture.
|
|
95
|
+
if (nativeBodyRegion !== undefined && regions.has(nativeBodyRegion) && placeholderType(regions.get(nativeBodyRegion)) !== 'body') {
|
|
96
|
+
const used = new Set(Object.values(indexes));
|
|
97
|
+
let idx = REGION_IDX_BASE.body;
|
|
98
|
+
while (used.has(idx)) idx++;
|
|
99
|
+
indexes[nativeBodyRegion] = idx;
|
|
100
|
+
}
|
|
101
|
+
return indexes;
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
// FNV-1a (64-bit) of the record's canonical JSON: evidence of which record the layout was made from.
|
|
@@ -196,7 +205,7 @@ export function layoutPlaceholders(template) {
|
|
|
196
205
|
const parsed = layoutTemplate(record);
|
|
197
206
|
const {areas} = composeLayoutAreas(record, {...template.areaOptions, textMeasurement: undefined});
|
|
198
207
|
const regions = new Map(parsed.regions.map(region => [region.name, region]));
|
|
199
|
-
const indexes = regionIndexes(record);
|
|
208
|
+
const indexes = regionIndexes(record, template.nativeBodyRegion);
|
|
200
209
|
const emu = box => ({x: Math.round(box.x * EMU), y: Math.round(box.y * EMU), cx: Math.max(0, Math.round(box.width * EMU)), cy: Math.max(0, Math.round(box.height * EMU))});
|
|
201
210
|
const result = [];
|
|
202
211
|
for (const area of areas) {
|
|
@@ -210,7 +219,8 @@ export function layoutPlaceholders(template) {
|
|
|
210
219
|
}
|
|
211
220
|
if (area.name === 'subtitle') { result.push({name: 'Subtitle', type: 'subTitle', idx: SUBTITLE_IDX, anchor: 't', ...emu(area.box)}); continue; }
|
|
212
221
|
const region = regions.get(area.name);
|
|
213
|
-
if (region) result.push({name: region.name, type: placeholderType(region), idx: indexes[region.name],
|
|
222
|
+
if (region) result.push({name: region.name, type: region.name === template.nativeBodyRegion ? 'body' : placeholderType(region), idx: indexes[region.name],
|
|
223
|
+
...(region.name === template.nativeBodyRegion ? {anchor: {top: 't', middle: 'ctr', bottom: 'b'}[region.anchor]} : {}), ...emu(area.box)});
|
|
214
224
|
}
|
|
215
225
|
const sample = composeSlide({title: 'Title', subtitle: 'Subtitle', blocks: [{text: 'Body'}]}, {...template.areaOptions, layout: record, textMeasurement: undefined});
|
|
216
226
|
for (const placeholder of result) {
|
|
@@ -244,7 +254,7 @@ function placeholderShape(placeholder, id) {
|
|
|
244
254
|
*/
|
|
245
255
|
export function layoutTagValue(template, {record = false} = {}) {
|
|
246
256
|
return {v: 1, id: template.id, group: template.group, ...(template.source !== undefined ? {source: template.source} : {}), reference: template.reference,
|
|
247
|
-
hash: recordHash(template.record), regions: Object.fromEntries(Object.entries(regionIndexes(template.record)).map(([name, idx]) => [String(idx), name])),
|
|
257
|
+
hash: recordHash(template.record), regions: Object.fromEntries(Object.entries(regionIndexes(template.record, template.nativeBodyRegion)).map(([name, idx]) => [String(idx), name])),
|
|
248
258
|
...(record && template.origin === 'document' ? {record: template.record} : {})};
|
|
249
259
|
}
|
|
250
260
|
|
|
@@ -297,6 +307,7 @@ export function writeTemplateLayouts(output, slides, templates, bindings = new M
|
|
|
297
307
|
const key = `${master}\u0001${template.key}`;
|
|
298
308
|
const path = `ppt/slideLayouts/slideLayout${nextLayout++}.xml`, tagPath = `ppt/tags/opfLayout${nextTag++}.xml`;
|
|
299
309
|
let name = typeof template.record.name === 'string' && template.record.name.trim() ? template.record.name : template.id;
|
|
310
|
+
if (template.nativeBodyRegion) name += ' (native body)';
|
|
300
311
|
if (taken.has(name)) name = `${name} (${template.group}:${template.id})`;
|
|
301
312
|
taken.add(name);
|
|
302
313
|
const type = POWERPOINT_LAYOUT_TYPES[template.id] ?? 'cust';
|
|
@@ -329,17 +340,22 @@ export function writeTemplateLayouts(output, slides, templates, bindings = new M
|
|
|
329
340
|
}
|
|
330
341
|
}
|
|
331
342
|
// Automatic composition is its own titleOnly layout, independently of catalog region templates.
|
|
332
|
-
for (const [master, base] of masters) {
|
|
333
|
-
const first = automaticSlides.find((plan, index) => plan && related(related(slidePaths[index], 'slideLayout'), 'slideMaster') === master);
|
|
343
|
+
for (const [master, base] of masters) for (const native of [false, true]) {
|
|
344
|
+
const first = automaticSlides.find((plan, index) => plan && Boolean(plan.nativeBody) === native && related(related(slidePaths[index], 'slideLayout'), 'slideMaster') === master);
|
|
334
345
|
if (!first) continue;
|
|
335
346
|
const path = `ppt/slideLayouts/slideLayout${nextLayout++}.xml`, tagPath = `ppt/tags/opfLayout${nextTag++}.xml`;
|
|
336
347
|
// Reuse accepted title geometry; an empty slide needs only the engine's default prompt, never a re-fit through the caller's measurement provider.
|
|
337
348
|
const title = first.title ?? composeSlide({title: 'Title'}, {...first.areaOptions, layout: undefined, textMeasurement: undefined}).items.find(item => item.field === 'title');
|
|
338
349
|
const placeholder = {name: 'Title', type: 'title', x: Math.round(title.box.x * EMU), y: Math.round(title.box.y * EMU), cx: Math.round(title.box.width * EMU), cy: Math.round(title.box.height * EMU), style: {...title.textStyle, fontSize: title.text.fontSize, alignment: title.alignment}};
|
|
339
|
-
|
|
340
|
-
|
|
350
|
+
const body = native ? {name: 'Body', type: 'body', idx: REGION_IDX_BASE.body,
|
|
351
|
+
...Object.fromEntries(['x', 'y'].map(key => [key, Math.round(first.nativeBody.box[key] * EMU)])),
|
|
352
|
+
cx: Math.round(first.nativeBody.box.width * EMU), cy: Math.round(first.nativeBody.box.height * EMU),
|
|
353
|
+
anchor: {top: 't', middle: 'ctr', bottom: 'b'}[first.nativeBody.anchor],
|
|
354
|
+
style: {...first.nativeBody.item.textStyle, fontSize: first.nativeBody.item.text.fontSize, alignment: first.nativeBody.item.alignment}} : undefined;
|
|
355
|
+
let xml = read(base).replace(/<p:sldLayout\b[^>]*>/, open => `${open.slice(0, -1).replace(/\s+(?:type|userDrawn|showMasterSp)="[^"]*"/g, '')} type="${native ? 'tx' : 'titleOnly'}" userDrawn="1">`);
|
|
356
|
+
xml = xml.replace(/(<p:cSld\b[^>]*?)(\s+name="[^"]*")?(\s*>)/, (match, open, _name, close) => `${open} name="${native ? 'OPF auto (native body)' : 'OPF auto'}"${close}`);
|
|
341
357
|
const nextId = Math.max(1, ...[...xml.matchAll(/<p:cNvPr\b[^>]*\bid="(\d+)"/g)].map(match => Number(match[1]))) + 1;
|
|
342
|
-
xml = xml.replace('</p:grpSpPr>', `</p:grpSpPr>${placeholderShape(placeholder, nextId)}`);
|
|
358
|
+
xml = xml.replace('</p:grpSpPr>', `</p:grpSpPr>${placeholderShape(placeholder, nextId)}${body ? placeholderShape(body, nextId + 1) : ''}`);
|
|
343
359
|
const masterRel = relationships(read(relsOf(base))).find(rel => rel.type === `${REL}/slideMaster`);
|
|
344
360
|
const tagged = addRelationship(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships">${masterRel.node}</Relationships>`, `${REL}/tags`, relativeTarget(path, tagPath));
|
|
345
361
|
xml = xml.replace('</p:spTree>', `</p:spTree><p:custDataLst><p:tags r:id="${tagged.rid}"/></p:custDataLst>`);
|
|
@@ -349,13 +365,13 @@ export function writeTemplateLayouts(output, slides, templates, bindings = new M
|
|
|
349
365
|
const added = addRelationship(read(relsOf(master)), `${REL}/slideLayout`, relativeTarget(master, path));
|
|
350
366
|
write(relsOf(master), added.relsXml);
|
|
351
367
|
write(master, read(master).replace('</p:sldLayoutIdLst>', `<p:sldLayoutId id="${nextLayoutId++}" r:id="${added.rid}"/></p:sldLayoutIdLst>`));
|
|
352
|
-
created.set(`${master}\
|
|
368
|
+
created.set(`${master}\u0001${native ? 'auto-native-body' : 'auto'}`, {path, placeholders: new Map([['Title', placeholder], ...(body ? [['Body', body]] : [])])});
|
|
353
369
|
}
|
|
354
370
|
for (const [index, plan] of slidePaths.map((_, index) => slides[index]).entries()) {
|
|
355
371
|
if (!plan && !automaticSlides[index]) continue;
|
|
356
372
|
const slidePath = slidePaths[index];
|
|
357
373
|
const master = related(related(slidePath, 'slideLayout'), 'slideMaster');
|
|
358
|
-
const layout = created.get(`${master}\u0001${plan ? plan.template.key : 'auto'}`);
|
|
374
|
+
const layout = created.get(`${master}\u0001${plan ? plan.template.key : automaticSlides[index].nativeBody ? 'auto-native-body' : 'auto'}`);
|
|
359
375
|
if (!layout) throw new Error(`Slide ${index + 1} has no template layout.`);
|
|
360
376
|
const slideRels = relsOf(slidePath);
|
|
361
377
|
write(slideRels, read(slideRels).replace(/<Relationship\b[^>]*\/>/g, node => attribute(node, 'Type') === `${REL}/slideLayout` ? node.replace(/\sTarget="[^"]*"/, ` Target="${relativeTarget(slidePath, layout.path)}"`) : node));
|
|
@@ -366,7 +382,7 @@ export function writeTemplateLayouts(output, slides, templates, bindings = new M
|
|
|
366
382
|
const placeholder = layout.placeholders.get(binding.region);
|
|
367
383
|
if (!placeholder || !bindsObject(placeholder.type, binding.object)) continue;
|
|
368
384
|
const ph = `<p:ph${placeholder.type === 'obj' ? '' : ` type="${placeholder.type}"`}${placeholder.idx !== undefined ? ` idx="${placeholder.idx}"` : ''}/>`;
|
|
369
|
-
const element = binding.object === 'picture' ? 'pic' : binding.object
|
|
385
|
+
const element = binding.object === 'picture' ? 'pic' : ['title', 'body'].includes(binding.object) ? 'sp' : 'graphicFrame';
|
|
370
386
|
const pattern = new RegExp(`<p:${element}>[\\s\\S]*?<\\/p:${element}>`, 'g');
|
|
371
387
|
slideXml = slideXml.replace(pattern, shape => {
|
|
372
388
|
if (unescapeAttribute(shape.match(/<p:cNvPr\b[^>]*\bname="([^"]*)"/)?.[1] ?? '') !== objectName || /<p:ph\b/.test(shape)) return shape;
|
package/dist/text-provenance.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import {XMLParser} from 'fast-xml-parser';
|
|
2
|
-
import {attachTextTags,decodeTextTag} from './code-provenance.js';
|
|
2
|
+
import {attachTextTags,decodeTextTag,groupUnchanged} from './code-provenance.js';
|
|
3
3
|
const TAG='OPF_TEXT_V1',REL='http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags';
|
|
4
4
|
const parser=new XMLParser({ignoreAttributes:false,attributeNamePrefix:'',parseTagValue:false,trimValues:false});
|
|
5
5
|
const decoder=new TextDecoder('utf-8',{fatal:true}),array=value=>value===undefined?[]:Array.isArray(value)?value:[value];
|
|
6
|
-
export const attachPlainTextTags=(entries,records)=>attachTextTags(entries,records,TAG,'opfText','plain text');
|
|
6
|
+
export const attachPlainTextTags=(entries,records)=>attachTextTags(entries,records,TAG,'opfText','plain text',{signed:true});
|
|
7
7
|
|
|
8
8
|
// Only boundaries are stored. Current native characters, including edits and
|
|
9
9
|
// empty lines, always supply the text; tags cannot restore discarded old words.
|
|
@@ -48,7 +48,7 @@ export function importPlainTextGroups(shapes,paragraphs,relationships,entries,re
|
|
|
48
48
|
const restored=sourceLineParagraphs(ordered,paragraphs);
|
|
49
49
|
for(const item of ordered)consumed.add(item.shape);
|
|
50
50
|
items.push({shapes:ordered.map(item=>item.shape),paragraphs:restored});
|
|
51
|
-
report({code:'text-import-reflow',message:'Complete tagged lines retain current native text, whitespace and authored line boundaries. Native formatting, geometry and OPF nesting are not reconstructed; review the reflowed text.'});
|
|
51
|
+
if(!groupUnchanged(ordered,paragraphs))report({code:'text-import-reflow',message:'Complete tagged lines retain current native text, whitespace and authored line boundaries. Native formatting, geometry and OPF nesting are not reconstructed; review the reflowed text.'});
|
|
52
52
|
}catch(error){report({code:'invalid-text-provenance',message:`${error.message} Ordinary import retains current native text without restoring old source words.`});}
|
|
53
53
|
return {consumed,items};
|
|
54
54
|
}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {XMLParser} from 'fast-xml-parser';
|
|
2
2
|
import {isValidFormat} from './format-check.js';
|
|
3
|
-
import {attachTextTags,decodeTextTag} from './code-provenance.js';
|
|
3
|
+
import {attachTextTags,decodeTextTag,groupUnchanged} from './code-provenance.js';
|
|
4
4
|
import {sourceLineParagraphs} from './text-provenance.js';
|
|
5
5
|
|
|
6
6
|
const STATUSES=['done','current','planned'];
|
|
7
7
|
const TAG='OPF_TIMELINE_V1',REL='http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags';
|
|
8
8
|
const parser=new XMLParser({ignoreAttributes:false,attributeNamePrefix:'',parseTagValue:false,trimValues:false});
|
|
9
9
|
const decoder=new TextDecoder('utf-8',{fatal:true}),array=value=>value===undefined?[]:Array.isArray(value)?value:[value];
|
|
10
|
-
export const attachTimelineTags=(entries,records)=>attachTextTags(entries,records,TAG,'opfTimeline','timeline');
|
|
10
|
+
export const attachTimelineTags=(entries,records)=>attachTextTags(entries,records,TAG,'opfTimeline','timeline',{signed:true});
|
|
11
11
|
|
|
12
12
|
// Only topology and line boundaries are retained. Every field value comes from
|
|
13
13
|
// current native text; clearing or editing a field cannot restore old words.
|
|
@@ -86,7 +86,7 @@ export function importTimelineGroups(shapes,paragraphs,relationships,entries,rep
|
|
|
86
86
|
if(!isValidFormat({slides:[{timeline}]}))throw Error('Current native text does not form a valid timeline.');
|
|
87
87
|
for(const item of group)consumed.add(item.shape);
|
|
88
88
|
items.push({shapes:group.map(item=>item.shape),payload:{type:'timeline',timeline}});
|
|
89
|
-
report({code:'timeline-import-reflow',message:'Timeline field order, source boundaries and current native text were recovered. Native formatting, positions, marker styling and font theme are not reconstructed; review the reflowed timeline.'});
|
|
89
|
+
if(!groupUnchanged(group,paragraphs))report({code:'timeline-import-reflow',message:'Timeline field order, source boundaries and current native text were recovered. Native formatting, positions, marker styling and font theme are not reconstructed; review the reflowed timeline.'});
|
|
90
90
|
}catch(error){report({code:'invalid-timeline-provenance',message:`${error.message} Ordinary import retains current native text without restoring old source words.`});}
|
|
91
91
|
return {consumed,items};
|
|
92
92
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {checkInputEntries,readInputZip,readNestedInputZip} from './input-safety.js';
|
|
2
2
|
|
|
3
3
|
// Typeface inventory for a PPTX package (FF-08, font-fidelity-everywhere).
|
|
4
4
|
//
|
|
@@ -45,7 +45,7 @@ export function inventoryTypefaces(input, options = {}) {
|
|
|
45
45
|
for (const path of Object.keys(entries).sort()) {
|
|
46
46
|
const bytes = entries[path];
|
|
47
47
|
if (NESTED_PACKAGE.test(path)) {
|
|
48
|
-
if (options.nested !== false) visit(
|
|
48
|
+
if (options.nested !== false) visit(readNestedInputZip(entries,path), `${prefix}${path}!/`);
|
|
49
49
|
continue;
|
|
50
50
|
}
|
|
51
51
|
if (!XML_PART.test(path)) continue;
|
|
@@ -162,9 +162,9 @@ export function compareFontNames(left, right) {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
function toEntries(input) {
|
|
165
|
-
if (input instanceof Uint8Array) return
|
|
166
|
-
if (input instanceof ArrayBuffer) return
|
|
167
|
-
if (input && typeof input === 'object') return input;
|
|
165
|
+
if (input instanceof Uint8Array) return readInputZip(input);
|
|
166
|
+
if (input instanceof ArrayBuffer) return readInputZip(new Uint8Array(input));
|
|
167
|
+
if (input && typeof input === 'object') return checkInputEntries(input);
|
|
168
168
|
throw new TypeError('Expected PPTX bytes or a map of package parts.');
|
|
169
169
|
}
|
|
170
170
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@openpresentation/opf-pptx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "Pure local OPF to PPTX export and PPTX to OPF import tooling.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
"dependencies": {
|
|
67
67
|
"@node-projects/jszip": "4.3.0",
|
|
68
68
|
"@openpresentation/gallery": "^3.0.0",
|
|
69
|
-
"@openpresentation/opf": "^0.
|
|
69
|
+
"@openpresentation/opf": "^0.22.1",
|
|
70
70
|
"@rgrove/parse-xml": "4.2.3",
|
|
71
71
|
"fast-xml-parser": "^5.8.0",
|
|
72
72
|
"fflate": "^0.8.3",
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
"sharp": "0.35.5"
|
|
75
75
|
},
|
|
76
76
|
"peerDependencies": {
|
|
77
|
-
"@openpresentation/opf-render": "^0.
|
|
77
|
+
"@openpresentation/opf-render": "^0.22.1"
|
|
78
78
|
},
|
|
79
79
|
"peerDependenciesMeta": {
|
|
80
80
|
"@openpresentation/opf-render": {
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"@expo-google-fonts/roboto": "0.4.3",
|
|
99
99
|
"@expo-google-fonts/roboto-mono": "0.4.2",
|
|
100
100
|
"@expo-google-fonts/tinos": "0.4.2",
|
|
101
|
-
"@openpresentation/opf-render": "0.
|
|
101
|
+
"@openpresentation/opf-render": "0.22.1",
|
|
102
102
|
"@resvg/resvg-js": "2.6.2",
|
|
103
103
|
"esbuild": "0.28.2",
|
|
104
104
|
"playwright": "1.64.0"
|