@opengeni/artifact-tool 0.2.2
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/LICENSE +190 -0
- package/README.md +104 -0
- package/dist/bounded-zip.d.ts +28 -0
- package/dist/chunk-4BSENMAE.js +39 -0
- package/dist/chunk-4BSENMAE.js.map +1 -0
- package/dist/chunk-4FRFNNBM.js +276 -0
- package/dist/chunk-4FRFNNBM.js.map +1 -0
- package/dist/chunk-5XIA6KHZ.js +49 -0
- package/dist/chunk-5XIA6KHZ.js.map +1 -0
- package/dist/chunk-66CT2ZRP.js +2229 -0
- package/dist/chunk-66CT2ZRP.js.map +1 -0
- package/dist/chunk-72SYDU4O.js +319 -0
- package/dist/chunk-72SYDU4O.js.map +1 -0
- package/dist/chunk-B2ZJRZDA.js +279 -0
- package/dist/chunk-B2ZJRZDA.js.map +1 -0
- package/dist/chunk-DCTLTNPW.js +2458 -0
- package/dist/chunk-DCTLTNPW.js.map +1 -0
- package/dist/chunk-FGZM4FQ6.js +473 -0
- package/dist/chunk-FGZM4FQ6.js.map +1 -0
- package/dist/chunk-HTJBLBDE.js +591 -0
- package/dist/chunk-HTJBLBDE.js.map +1 -0
- package/dist/chunk-IYFXZQ34.js +5306 -0
- package/dist/chunk-IYFXZQ34.js.map +1 -0
- package/dist/chunk-J6N7IR44.js +172 -0
- package/dist/chunk-J6N7IR44.js.map +1 -0
- package/dist/chunk-N2IHNWBS.js +1002 -0
- package/dist/chunk-N2IHNWBS.js.map +1 -0
- package/dist/chunk-NAG44K67.js +549 -0
- package/dist/chunk-NAG44K67.js.map +1 -0
- package/dist/chunk-O24KTEGP.js +33 -0
- package/dist/chunk-O24KTEGP.js.map +1 -0
- package/dist/chunk-PI2AZVGK.js +2074 -0
- package/dist/chunk-PI2AZVGK.js.map +1 -0
- package/dist/chunk-Q2FFHEY7.js +40 -0
- package/dist/chunk-Q2FFHEY7.js.map +1 -0
- package/dist/chunk-RBGQADXA.js +57 -0
- package/dist/chunk-RBGQADXA.js.map +1 -0
- package/dist/chunk-U5G2BKM2.js +613 -0
- package/dist/chunk-U5G2BKM2.js.map +1 -0
- package/dist/chunk-VOJHMSM3.js +39 -0
- package/dist/chunk-VOJHMSM3.js.map +1 -0
- package/dist/chunk-VUNV25KB.js +16 -0
- package/dist/chunk-VUNV25KB.js.map +1 -0
- package/dist/chunk-XIFUNVBO.js +101 -0
- package/dist/chunk-XIFUNVBO.js.map +1 -0
- package/dist/chunk-ZCJYRJGB.js +489 -0
- package/dist/chunk-ZCJYRJGB.js.map +1 -0
- package/dist/chunk-ZZXPEE4S.js +2260 -0
- package/dist/chunk-ZZXPEE4S.js.map +1 -0
- package/dist/document-docx-api.d.ts +49 -0
- package/dist/document-docx-codec.d.ts +9 -0
- package/dist/document-docx-codec.js +4765 -0
- package/dist/document-docx-codec.js.map +1 -0
- package/dist/document-docx-import.d.ts +261 -0
- package/dist/document-docx-packer.d.ts +18 -0
- package/dist/document-docx-state.d.ts +12 -0
- package/dist/document-render.d.ts +3 -0
- package/dist/document-render.js +492 -0
- package/dist/document-render.js.map +1 -0
- package/dist/document.d.ts +432 -0
- package/dist/document.js +37 -0
- package/dist/document.js.map +1 -0
- package/dist/errors.d.ts +23 -0
- package/dist/file-blob.d.ts +11 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +186 -0
- package/dist/index.js.map +1 -0
- package/dist/inspection.d.ts +12 -0
- package/dist/kernel.d.ts +101 -0
- package/dist/lib-GQLPACFK.js +9746 -0
- package/dist/lib-GQLPACFK.js.map +1 -0
- package/dist/materializer-cli-entry.d.ts +2 -0
- package/dist/materializer-cli-entry.js +29 -0
- package/dist/materializer-cli-entry.js.map +1 -0
- package/dist/materializer-cli.d.ts +24 -0
- package/dist/materializer-cli.js +24 -0
- package/dist/materializer-cli.js.map +1 -0
- package/dist/native-raster.d.ts +10 -0
- package/dist/native.d.ts +129 -0
- package/dist/native.js +31 -0
- package/dist/native.js.map +1 -0
- package/dist/office-import.d.ts +48 -0
- package/dist/office-import.js +154 -0
- package/dist/office-import.js.map +1 -0
- package/dist/presentation-pptx-api.d.ts +60 -0
- package/dist/presentation-pptx-import.d.ts +5 -0
- package/dist/presentation-pptx-state-digest-765CZZED.js +10 -0
- package/dist/presentation-pptx-state-digest-765CZZED.js.map +1 -0
- package/dist/presentation-pptx-state-digest.d.ts +3 -0
- package/dist/presentation-pptx-state.d.ts +4 -0
- package/dist/presentation-pptx-xml.d.ts +29 -0
- package/dist/presentation-pptx.d.ts +5 -0
- package/dist/presentation-pptx.js +2365 -0
- package/dist/presentation-pptx.js.map +1 -0
- package/dist/presentation-render.d.ts +29 -0
- package/dist/presentation-render.js +375 -0
- package/dist/presentation-render.js.map +1 -0
- package/dist/presentation.d.ts +565 -0
- package/dist/presentation.js +71 -0
- package/dist/presentation.js.map +1 -0
- package/dist/production-composite.d.ts +68 -0
- package/dist/production-document.d.ts +2 -0
- package/dist/production-document.js +176 -0
- package/dist/production-document.js.map +1 -0
- package/dist/production-native-codecs.d.ts +94 -0
- package/dist/production-presentation.d.ts +2 -0
- package/dist/production-presentation.js +176 -0
- package/dist/production-presentation.js.map +1 -0
- package/dist/production-sha256.d.ts +2 -0
- package/dist/production-spreadsheet-native.d.ts +12 -0
- package/dist/production-spreadsheet.d.ts +2 -0
- package/dist/production-spreadsheet.js +176 -0
- package/dist/production-spreadsheet.js.map +1 -0
- package/dist/production.d.ts +96 -0
- package/dist/raster-image.d.ts +34 -0
- package/dist/reference.d.ts +17 -0
- package/dist/reference.js +574 -0
- package/dist/reference.js.map +1 -0
- package/dist/runtime-cli-entry.d.ts +2 -0
- package/dist/runtime-cli-entry.js +21 -0
- package/dist/runtime-cli-entry.js.map +1 -0
- package/dist/runtime-cli.d.ts +76 -0
- package/dist/runtime-cli.js +26 -0
- package/dist/runtime-cli.js.map +1 -0
- package/dist/runtime-development.d.ts +73 -0
- package/dist/runtime-development.js +30 -0
- package/dist/runtime-development.js.map +1 -0
- package/dist/runtime-receipt.d.ts +19 -0
- package/dist/runtime.d.ts +355 -0
- package/dist/runtime.js +42 -0
- package/dist/runtime.js.map +1 -0
- package/dist/snapshot.d.ts +26 -0
- package/dist/snapshot.js +61 -0
- package/dist/snapshot.js.map +1 -0
- package/dist/spreadsheet-address.d.ts +18 -0
- package/dist/spreadsheet-file.d.ts +17 -0
- package/dist/spreadsheet-formula.d.ts +67 -0
- package/dist/spreadsheet-image.d.ts +17 -0
- package/dist/spreadsheet-render.d.ts +5 -0
- package/dist/spreadsheet-render.js +752 -0
- package/dist/spreadsheet-render.js.map +1 -0
- package/dist/spreadsheet-types.d.ts +155 -0
- package/dist/spreadsheet-xlsx-api.d.ts +88 -0
- package/dist/spreadsheet-xlsx-codec.d.ts +16 -0
- package/dist/spreadsheet-xlsx-codec.js +16 -0
- package/dist/spreadsheet-xlsx-codec.js.map +1 -0
- package/dist/spreadsheet-xlsx-state.d.ts +16 -0
- package/dist/spreadsheet.d.ts +724 -0
- package/dist/spreadsheet.js +67 -0
- package/dist/spreadsheet.js.map +1 -0
- package/package.json +129 -0
- package/src/bounded-zip.ts +450 -0
- package/src/document-docx-api.ts +64 -0
- package/src/document-docx-codec.ts +2253 -0
- package/src/document-docx-import.ts +3814 -0
- package/src/document-docx-packer.ts +72 -0
- package/src/document-docx-state.ts +59 -0
- package/src/document-render.ts +615 -0
- package/src/document.ts +2800 -0
- package/src/errors.ts +45 -0
- package/src/file-blob.ts +43 -0
- package/src/index.ts +4 -0
- package/src/inspection.ts +50 -0
- package/src/kernel.ts +525 -0
- package/src/materializer-cli-entry.ts +11 -0
- package/src/materializer-cli.ts +743 -0
- package/src/native-raster.ts +141 -0
- package/src/native.ts +390 -0
- package/src/office-import.ts +243 -0
- package/src/presentation-pptx-api.ts +107 -0
- package/src/presentation-pptx-import.ts +2065 -0
- package/src/presentation-pptx-state-digest.ts +46 -0
- package/src/presentation-pptx-state.ts +18 -0
- package/src/presentation-pptx-xml.ts +473 -0
- package/src/presentation-pptx.ts +447 -0
- package/src/presentation-render.ts +532 -0
- package/src/presentation.ts +2599 -0
- package/src/production-composite.ts +805 -0
- package/src/production-document.ts +2 -0
- package/src/production-native-codecs.ts +1141 -0
- package/src/production-presentation.ts +2 -0
- package/src/production-sha256.ts +150 -0
- package/src/production-spreadsheet-native.ts +686 -0
- package/src/production-spreadsheet.ts +2 -0
- package/src/production.ts +1022 -0
- package/src/raster-image.ts +586 -0
- package/src/reference.ts +17 -0
- package/src/runtime-cli-entry.ts +13 -0
- package/src/runtime-cli.ts +670 -0
- package/src/runtime-development.ts +689 -0
- package/src/runtime-receipt.ts +169 -0
- package/src/runtime.ts +1463 -0
- package/src/snapshot.ts +88 -0
- package/src/spreadsheet-address.ts +90 -0
- package/src/spreadsheet-file.ts +71 -0
- package/src/spreadsheet-formula.ts +1212 -0
- package/src/spreadsheet-image.ts +242 -0
- package/src/spreadsheet-render.ts +985 -0
- package/src/spreadsheet-types.ts +171 -0
- package/src/spreadsheet-xlsx-api.ts +123 -0
- package/src/spreadsheet-xlsx-codec.ts +3063 -0
- package/src/spreadsheet-xlsx-state.ts +191 -0
- package/src/spreadsheet.ts +5195 -0
|
@@ -0,0 +1,2065 @@
|
|
|
1
|
+
import {
|
|
2
|
+
inflateBoundedZipEntry,
|
|
3
|
+
parseBoundedZip,
|
|
4
|
+
verifyBoundedZipEntry,
|
|
5
|
+
type BoundedZipEntry,
|
|
6
|
+
type BoundedZipFailure,
|
|
7
|
+
} from "./bounded-zip";
|
|
8
|
+
import { FileBlob } from "./file-blob";
|
|
9
|
+
import {
|
|
10
|
+
Presentation,
|
|
11
|
+
PresentationFidelityError,
|
|
12
|
+
type PresentationChartConfig,
|
|
13
|
+
type PresentationChartSeriesConfig,
|
|
14
|
+
type PresentationChartType,
|
|
15
|
+
type PresentationFill,
|
|
16
|
+
type PresentationGroupChildConfig,
|
|
17
|
+
type PresentationGroupConfig,
|
|
18
|
+
type PresentationImageConfig,
|
|
19
|
+
type PresentationLayout,
|
|
20
|
+
type PresentationLine,
|
|
21
|
+
type PresentationMaster,
|
|
22
|
+
type PresentationPosition,
|
|
23
|
+
type PresentationShapeConfig,
|
|
24
|
+
type PresentationShapeGeometry,
|
|
25
|
+
type PresentationTableCellInput,
|
|
26
|
+
type PresentationTableConfig,
|
|
27
|
+
type PresentationTemplateElement,
|
|
28
|
+
type PresentationTextStyle,
|
|
29
|
+
} from "./presentation";
|
|
30
|
+
import {
|
|
31
|
+
PPTX_MEDIA_TYPE,
|
|
32
|
+
PresentationSecurityError,
|
|
33
|
+
type PresentationFidelityIssue,
|
|
34
|
+
type PresentationPptxImportLimits,
|
|
35
|
+
type PresentationPptxImportOptions,
|
|
36
|
+
} from "./presentation-pptx-api";
|
|
37
|
+
import { setPresentationLossState } from "./presentation-pptx-state";
|
|
38
|
+
import {
|
|
39
|
+
parsePptxXmlPart,
|
|
40
|
+
pptxXmlAttribute,
|
|
41
|
+
pptxXmlChild,
|
|
42
|
+
pptxXmlChildren,
|
|
43
|
+
pptxXmlDescendants,
|
|
44
|
+
pptxXmlDirectText,
|
|
45
|
+
pptxXmlQualifiedAttribute,
|
|
46
|
+
pptxXmlText,
|
|
47
|
+
type PptxXmlBudget,
|
|
48
|
+
type PptxXmlElement,
|
|
49
|
+
} from "./presentation-pptx-xml";
|
|
50
|
+
|
|
51
|
+
const OFFICE_RELATIONSHIP_BASE =
|
|
52
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/";
|
|
53
|
+
const PACKAGE_RELATIONSHIP_BASE = "http://schemas.openxmlformats.org/package/2006/relationships/";
|
|
54
|
+
const PRESENTATION_MAIN_TYPE =
|
|
55
|
+
"application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml";
|
|
56
|
+
const PRESENTATION_NAMESPACE = "http://schemas.openxmlformats.org/presentationml/2006/main";
|
|
57
|
+
const DRAWING_NAMESPACE = "http://schemas.openxmlformats.org/drawingml/2006/main";
|
|
58
|
+
const CHART_NAMESPACE = "http://schemas.openxmlformats.org/drawingml/2006/chart";
|
|
59
|
+
const CONTENT_TYPES_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/content-types";
|
|
60
|
+
const RELATIONSHIPS_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
61
|
+
const TABLE_GRAPHIC_URI = "http://schemas.openxmlformats.org/drawingml/2006/table";
|
|
62
|
+
const CHART_GRAPHIC_URI = "http://schemas.openxmlformats.org/drawingml/2006/chart";
|
|
63
|
+
const EMU_PER_PIXEL = 9_525;
|
|
64
|
+
|
|
65
|
+
export const DEFAULT_PPTX_IMPORT_LIMITS: PresentationPptxImportLimits = Object.freeze({
|
|
66
|
+
compressedBytes: 128 * 1024 * 1024,
|
|
67
|
+
expandedBytes: 256 * 1024 * 1024,
|
|
68
|
+
entryBytes: 64 * 1024 * 1024,
|
|
69
|
+
entries: 30_000,
|
|
70
|
+
compressionRatio: 100,
|
|
71
|
+
totalXmlBytes: 32 * 1024 * 1024,
|
|
72
|
+
xmlBytes: 8 * 1024 * 1024,
|
|
73
|
+
xmlDepth: 192,
|
|
74
|
+
xmlNodes: 2_000_000,
|
|
75
|
+
xmlAttributesPerElement: 512,
|
|
76
|
+
relationshipsPerPart: 8_192,
|
|
77
|
+
relationships: 50_000,
|
|
78
|
+
slides: 10_000,
|
|
79
|
+
elements: 1_000_000,
|
|
80
|
+
textCharacters: 16_000_000,
|
|
81
|
+
imageBytes: 128 * 1024 * 1024,
|
|
82
|
+
chartPoints: 5_000_000,
|
|
83
|
+
nestedPackageBytes: 32 * 1024 * 1024,
|
|
84
|
+
nestedExpandedBytes: 64 * 1024 * 1024,
|
|
85
|
+
nestedEntries: 20_000,
|
|
86
|
+
retainedBytes: 256 * 1024 * 1024,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
type Relationship = {
|
|
90
|
+
id: string;
|
|
91
|
+
type: string;
|
|
92
|
+
target: string;
|
|
93
|
+
sourcePart: string;
|
|
94
|
+
partName: string;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
type ImportContext = {
|
|
98
|
+
bytes: Uint8Array;
|
|
99
|
+
limits: PresentationPptxImportLimits;
|
|
100
|
+
entries: ReadonlyMap<string, BoundedZipEntry>;
|
|
101
|
+
xmlBudget: PptxXmlBudget;
|
|
102
|
+
xmlCache: Map<string, PptxXmlElement>;
|
|
103
|
+
bytesCache: Map<string, Uint8Array>;
|
|
104
|
+
relationshipsCache: Map<string, readonly Relationship[]>;
|
|
105
|
+
issues: PresentationFidelityIssue[];
|
|
106
|
+
unsupportedParts: Set<string>;
|
|
107
|
+
elements: number;
|
|
108
|
+
textCharacters: number;
|
|
109
|
+
imageBytes: number;
|
|
110
|
+
chartPoints: number;
|
|
111
|
+
relationships: number;
|
|
112
|
+
nestedExpandedBytes: number;
|
|
113
|
+
nestedEntries: number;
|
|
114
|
+
retainedBytes: number;
|
|
115
|
+
themeColors: ReadonlyMap<string, string>;
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
type Transform = {
|
|
119
|
+
offsetX: number;
|
|
120
|
+
offsetY: number;
|
|
121
|
+
scaleX: number;
|
|
122
|
+
scaleY: number;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
type ParsedElement = PresentationGroupChildConfig & { placeholderType?: string };
|
|
126
|
+
|
|
127
|
+
export async function importPresentationPptx(
|
|
128
|
+
input: FileBlob | Blob | ArrayBuffer | Uint8Array,
|
|
129
|
+
options: PresentationPptxImportOptions = {},
|
|
130
|
+
): Promise<Presentation> {
|
|
131
|
+
const limits = resolveLimits(options.limits);
|
|
132
|
+
const bytes = await toBytes(input);
|
|
133
|
+
if (bytes.byteLength === 0 || bytes.byteLength > limits.compressedBytes) {
|
|
134
|
+
throw pptxSecurity(
|
|
135
|
+
bytes.byteLength === 0 ? "PPTX input is empty" : "PPTX exceeds its compressed byte limit",
|
|
136
|
+
bytes.byteLength === 0 ? "invalid-package" : "limit-exceeded",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
const directory = parseBoundedZip(
|
|
140
|
+
bytes,
|
|
141
|
+
{
|
|
142
|
+
entries: limits.entries,
|
|
143
|
+
compressedEntryBytes: limits.compressedBytes,
|
|
144
|
+
expandedEntryBytes: limits.entryBytes,
|
|
145
|
+
expandedBytes: limits.expandedBytes,
|
|
146
|
+
compressionRatio: limits.compressionRatio,
|
|
147
|
+
compressionRatioThreshold: 1_048_576,
|
|
148
|
+
},
|
|
149
|
+
pptxZipFailure,
|
|
150
|
+
);
|
|
151
|
+
const entries = new Map(directory.map((entry) => [entry.name, entry]));
|
|
152
|
+
const context: ImportContext = {
|
|
153
|
+
bytes,
|
|
154
|
+
limits,
|
|
155
|
+
entries,
|
|
156
|
+
xmlBudget: { limits, totalBytes: 0, totalNodes: 0 },
|
|
157
|
+
xmlCache: new Map(),
|
|
158
|
+
bytesCache: new Map(),
|
|
159
|
+
relationshipsCache: new Map(),
|
|
160
|
+
issues: [],
|
|
161
|
+
unsupportedParts: new Set(),
|
|
162
|
+
elements: 0,
|
|
163
|
+
textCharacters: 0,
|
|
164
|
+
imageBytes: 0,
|
|
165
|
+
chartPoints: 0,
|
|
166
|
+
relationships: 0,
|
|
167
|
+
nestedExpandedBytes: 0,
|
|
168
|
+
nestedEntries: 0,
|
|
169
|
+
retainedBytes: bytes.byteLength,
|
|
170
|
+
themeColors: DEFAULT_THEME_COLORS,
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
await preflightPackage(context);
|
|
174
|
+
const rootRelationships = relationshipsFor(context, "");
|
|
175
|
+
const officeDocuments = rootRelationships.filter(
|
|
176
|
+
(relationship) => relationship.type === `${OFFICE_RELATIONSHIP_BASE}officeDocument`,
|
|
177
|
+
);
|
|
178
|
+
if (officeDocuments.length !== 1 || officeDocuments[0]?.partName !== "ppt/presentation.xml") {
|
|
179
|
+
throw pptxSecurity(
|
|
180
|
+
"PPTX must contain exactly one canonical ppt/presentation.xml office document",
|
|
181
|
+
"invalid-package",
|
|
182
|
+
"_rels/.rels",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
const presentationRoot = readXml(context, "ppt/presentation.xml");
|
|
186
|
+
requireRoot(presentationRoot, "presentation", PRESENTATION_NAMESPACE, "ppt/presentation.xml");
|
|
187
|
+
const presentationRelationships = relationshipsFor(context, "ppt/presentation.xml");
|
|
188
|
+
const presentationRelationshipById = byRelationshipId(presentationRelationships);
|
|
189
|
+
const slideSize = parseSlideSize(presentationRoot);
|
|
190
|
+
|
|
191
|
+
const themeRelationship = presentationRelationships.find(
|
|
192
|
+
(relationship) => relationship.type === `${OFFICE_RELATIONSHIP_BASE}theme`,
|
|
193
|
+
);
|
|
194
|
+
if (themeRelationship) {
|
|
195
|
+
context.themeColors = parseThemeColors(
|
|
196
|
+
readXml(context, themeRelationship.partName),
|
|
197
|
+
themeRelationship.partName,
|
|
198
|
+
);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const presentation = Presentation.create({ slideSize });
|
|
202
|
+
const masterByPart = new Map<string, PresentationMaster>();
|
|
203
|
+
const layoutByPart = new Map<string, PresentationLayout>();
|
|
204
|
+
importMastersAndLayouts(
|
|
205
|
+
context,
|
|
206
|
+
presentation,
|
|
207
|
+
presentationRoot,
|
|
208
|
+
presentationRelationshipById,
|
|
209
|
+
masterByPart,
|
|
210
|
+
layoutByPart,
|
|
211
|
+
);
|
|
212
|
+
importSlides(context, presentation, presentationRoot, presentationRelationshipById, layoutByPart);
|
|
213
|
+
|
|
214
|
+
const deduplicatedIssues = deduplicateIssues(context.issues);
|
|
215
|
+
if (options.unsupportedContent === "error" && deduplicatedIssues.length > 0) {
|
|
216
|
+
throw new PresentationFidelityError(
|
|
217
|
+
"PPTX contains inert content that the editable presentation model cannot regenerate exactly",
|
|
218
|
+
deduplicatedIssues,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
const { presentationModelDigest, sha256Hex } = await import("./presentation-pptx-state-digest");
|
|
222
|
+
setPresentationLossState(presentation, {
|
|
223
|
+
version: 1,
|
|
224
|
+
mediaType: PPTX_MEDIA_TYPE,
|
|
225
|
+
sourceBytes: bytes,
|
|
226
|
+
sourceDigest: await sha256Hex(bytes),
|
|
227
|
+
unsupportedParts: [...context.unsupportedParts].sort(),
|
|
228
|
+
modelDigest: await presentationModelDigest(presentation),
|
|
229
|
+
});
|
|
230
|
+
return presentation;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function resolveLimits(
|
|
234
|
+
requested: Partial<PresentationPptxImportLimits> | undefined,
|
|
235
|
+
): PresentationPptxImportLimits {
|
|
236
|
+
const limits = { ...DEFAULT_PPTX_IMPORT_LIMITS, ...requested };
|
|
237
|
+
for (const [name, value] of Object.entries(limits)) {
|
|
238
|
+
if (!Number.isSafeInteger(value) || value <= 0) {
|
|
239
|
+
throw pptxSecurity(`Invalid PPTX import limit: ${name}`, "limit-exceeded");
|
|
240
|
+
}
|
|
241
|
+
const maximum = DEFAULT_PPTX_IMPORT_LIMITS[name as keyof PresentationPptxImportLimits];
|
|
242
|
+
if (value > maximum) {
|
|
243
|
+
throw pptxSecurity(
|
|
244
|
+
`PPTX import limit ${name} may only tighten the production maximum`,
|
|
245
|
+
"limit-exceeded",
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return limits;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function toBytes(input: FileBlob | Blob | ArrayBuffer | Uint8Array): Promise<Uint8Array> {
|
|
253
|
+
if (input instanceof Uint8Array) return input.slice();
|
|
254
|
+
if (input instanceof ArrayBuffer) return new Uint8Array(input.slice(0));
|
|
255
|
+
if (input instanceof Blob) return new Uint8Array(await input.arrayBuffer());
|
|
256
|
+
throw pptxSecurity("PPTX input must be a Blob, ArrayBuffer, or Uint8Array", "invalid-package");
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async function preflightPackage(context: ImportContext): Promise<void> {
|
|
260
|
+
if (!context.entries.has("[Content_Types].xml") || !context.entries.has("_rels/.rels")) {
|
|
261
|
+
throw pptxSecurity("PPTX is missing required OPC package parts", "invalid-package");
|
|
262
|
+
}
|
|
263
|
+
for (const entry of context.entries.values()) rejectActiveEntry(entry.name, entry.directory);
|
|
264
|
+
for (const entry of context.entries.values()) {
|
|
265
|
+
if (entry.directory) continue;
|
|
266
|
+
if (isXmlPart(entry.name)) {
|
|
267
|
+
const xmlBytes = await inflateBoundedZipEntry(
|
|
268
|
+
context.bytes,
|
|
269
|
+
entry,
|
|
270
|
+
context.limits.entryBytes,
|
|
271
|
+
pptxZipFailure,
|
|
272
|
+
);
|
|
273
|
+
reserveRetained(context, xmlBytes.byteLength * 3, entry.name);
|
|
274
|
+
context.bytesCache.set(entry.name, xmlBytes);
|
|
275
|
+
readXml(context, entry.name);
|
|
276
|
+
} else if (isRasterMediaPart(entry.name)) {
|
|
277
|
+
const imageBytes = await inflateBoundedZipEntry(
|
|
278
|
+
context.bytes,
|
|
279
|
+
entry,
|
|
280
|
+
context.limits.entryBytes,
|
|
281
|
+
pptxZipFailure,
|
|
282
|
+
);
|
|
283
|
+
reserveRetained(context, imageBytes.byteLength, entry.name);
|
|
284
|
+
context.bytesCache.set(entry.name, imageBytes);
|
|
285
|
+
} else if (/^ppt\/embeddings\/.*\.xlsx$/i.test(entry.name)) {
|
|
286
|
+
const nestedBytes = await inflateBoundedZipEntry(
|
|
287
|
+
context.bytes,
|
|
288
|
+
entry,
|
|
289
|
+
context.limits.nestedPackageBytes,
|
|
290
|
+
pptxZipFailure,
|
|
291
|
+
);
|
|
292
|
+
await preflightEmbeddedXlsx(context, entry, nestedBytes);
|
|
293
|
+
} else {
|
|
294
|
+
await verifyBoundedZipEntry(context.bytes, entry, context.limits.entryBytes, pptxZipFailure);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
validateContentTypes(context);
|
|
298
|
+
for (const entry of context.entries.values()) {
|
|
299
|
+
if (!entry.name.endsWith(".rels")) continue;
|
|
300
|
+
relationshipsForPart(context, entry.name);
|
|
301
|
+
}
|
|
302
|
+
validateRelationshipTargets(context);
|
|
303
|
+
classifySourceOnlyParts(context);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function isXmlPart(name: string): boolean {
|
|
307
|
+
return name === "[Content_Types].xml" || /(?:\.xml|\.rels)$/i.test(name);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function readXml(context: ImportContext, partName: string): PptxXmlElement {
|
|
311
|
+
const cached = context.xmlCache.get(partName);
|
|
312
|
+
if (cached) return cached;
|
|
313
|
+
const bytes = readPartBytes(context, partName);
|
|
314
|
+
const parsed = parsePptxXmlPart(bytes, partName, context.xmlBudget);
|
|
315
|
+
context.xmlCache.set(partName, parsed);
|
|
316
|
+
context.bytesCache.delete(partName);
|
|
317
|
+
return parsed;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function readPartBytes(context: ImportContext, partName: string): Uint8Array {
|
|
321
|
+
const cached = context.bytesCache.get(partName);
|
|
322
|
+
if (cached) return cached;
|
|
323
|
+
const entry = context.entries.get(partName);
|
|
324
|
+
if (!entry || entry.directory) {
|
|
325
|
+
throw pptxSecurity("Required PPTX part is missing", "invalid-package", partName);
|
|
326
|
+
}
|
|
327
|
+
// XML is inflated synchronously by preflight before projection; cached access is sync.
|
|
328
|
+
throw pptxSecurity(
|
|
329
|
+
"PPTX part was not inflated during bounded preflight",
|
|
330
|
+
"invalid-package",
|
|
331
|
+
partName,
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function validateContentTypes(context: ImportContext): void {
|
|
336
|
+
const root = readXml(context, "[Content_Types].xml");
|
|
337
|
+
requireRoot(root, "Types", CONTENT_TYPES_NAMESPACE, "[Content_Types].xml");
|
|
338
|
+
const defaults = new Map<string, string>();
|
|
339
|
+
const overrides = new Map<string, string>();
|
|
340
|
+
for (const child of pptxXmlChildren(root)) {
|
|
341
|
+
if (child.localName === "Default") {
|
|
342
|
+
const extension = requiredAttribute(child, "Extension", "[Content_Types].xml").toLowerCase();
|
|
343
|
+
const contentType = requiredAttribute(child, "ContentType", "[Content_Types].xml");
|
|
344
|
+
if (defaults.has(extension)) {
|
|
345
|
+
throw pptxSecurity(
|
|
346
|
+
"Duplicate OPC content-type extension",
|
|
347
|
+
"invalid-package",
|
|
348
|
+
"[Content_Types].xml",
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
defaults.set(extension, contentType);
|
|
352
|
+
if (!KNOWN_DEFAULT_CONTENT_TYPES.has(`${extension}\0${contentType}`)) {
|
|
353
|
+
markSourceOnly(context, ["[Content_Types].xml"]);
|
|
354
|
+
}
|
|
355
|
+
} else if (child.localName === "Override") {
|
|
356
|
+
const name = requiredAttribute(child, "PartName", "[Content_Types].xml");
|
|
357
|
+
if (!name.startsWith("/") || name.includes("\\") || name.includes("..")) {
|
|
358
|
+
throw pptxSecurity(
|
|
359
|
+
"Unsafe OPC content-type part name",
|
|
360
|
+
"invalid-package",
|
|
361
|
+
"[Content_Types].xml",
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
const normalized = name.slice(1);
|
|
365
|
+
const contentType = requiredAttribute(child, "ContentType", "[Content_Types].xml");
|
|
366
|
+
if (overrides.has(normalized.toLowerCase())) {
|
|
367
|
+
throw pptxSecurity("Duplicate OPC content-type override", "invalid-package", normalized);
|
|
368
|
+
}
|
|
369
|
+
overrides.set(normalized.toLowerCase(), contentType);
|
|
370
|
+
if (!isKnownPptxPart(normalized)) {
|
|
371
|
+
markSourceOnly(context, ["[Content_Types].xml", normalized]);
|
|
372
|
+
}
|
|
373
|
+
} else {
|
|
374
|
+
throw pptxSecurity(
|
|
375
|
+
"Unknown OPC content-type declaration",
|
|
376
|
+
"invalid-package",
|
|
377
|
+
"[Content_Types].xml",
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const presentationType = overrides.get("ppt/presentation.xml");
|
|
382
|
+
if (presentationType !== PRESENTATION_MAIN_TYPE) {
|
|
383
|
+
throw pptxSecurity(
|
|
384
|
+
presentationType?.includes("macroEnabled")
|
|
385
|
+
? "Macro-enabled presentations are forbidden"
|
|
386
|
+
: "ppt/presentation.xml has the wrong content type",
|
|
387
|
+
presentationType?.includes("macroEnabled") ? "active-content" : "invalid-package",
|
|
388
|
+
"[Content_Types].xml",
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
for (const entry of context.entries.values()) {
|
|
392
|
+
if (entry.directory || entry.name === "[Content_Types].xml") continue;
|
|
393
|
+
const extension = entry.name.includes(".")
|
|
394
|
+
? entry.name.slice(entry.name.lastIndexOf(".") + 1).toLowerCase()
|
|
395
|
+
: "";
|
|
396
|
+
const contentType = overrides.get(entry.name.toLowerCase()) ?? defaults.get(extension);
|
|
397
|
+
if (!contentType) {
|
|
398
|
+
throw pptxSecurity("OPC package part lacks a content type", "invalid-package", entry.name);
|
|
399
|
+
}
|
|
400
|
+
if (isActiveContentType(contentType)) {
|
|
401
|
+
throw pptxSecurity("Active OPC content type is forbidden", "active-content", entry.name);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function relationshipsFor(context: ImportContext, sourcePart: string): readonly Relationship[] {
|
|
407
|
+
return relationshipsForPart(context, relationshipPartName(sourcePart));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function relationshipsForPart(
|
|
411
|
+
context: ImportContext,
|
|
412
|
+
relationshipsPart: string,
|
|
413
|
+
): readonly Relationship[] {
|
|
414
|
+
const cached = context.relationshipsCache.get(relationshipsPart);
|
|
415
|
+
if (cached) return cached;
|
|
416
|
+
if (!context.entries.has(relationshipsPart)) {
|
|
417
|
+
if (relationshipsPart === "_rels/.rels") {
|
|
418
|
+
throw pptxSecurity(
|
|
419
|
+
"OPC root relationships are missing",
|
|
420
|
+
"invalid-package",
|
|
421
|
+
relationshipsPart,
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
context.relationshipsCache.set(relationshipsPart, []);
|
|
425
|
+
return [];
|
|
426
|
+
}
|
|
427
|
+
const root = readXml(context, relationshipsPart);
|
|
428
|
+
requireRoot(root, "Relationships", RELATIONSHIPS_NAMESPACE, relationshipsPart);
|
|
429
|
+
const sourcePart = relationshipSourcePart(relationshipsPart);
|
|
430
|
+
const relationships: Relationship[] = [];
|
|
431
|
+
const ids = new Set<string>();
|
|
432
|
+
for (const child of pptxXmlChildren(root)) {
|
|
433
|
+
if (child.localName !== "Relationship" || pptxXmlChildren(child).length > 0) {
|
|
434
|
+
throw pptxSecurity("Invalid OPC relationship element", "invalid-package", relationshipsPart);
|
|
435
|
+
}
|
|
436
|
+
if (relationships.length >= context.limits.relationshipsPerPart) {
|
|
437
|
+
throw pptxSecurity(
|
|
438
|
+
"Relationship part exceeds its relationship limit",
|
|
439
|
+
"limit-exceeded",
|
|
440
|
+
relationshipsPart,
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
const id = requiredAttribute(child, "Id", relationshipsPart);
|
|
444
|
+
const type = requiredAttribute(child, "Type", relationshipsPart);
|
|
445
|
+
const target = requiredAttribute(child, "Target", relationshipsPart);
|
|
446
|
+
if (id.length > 512 || type.length > 2_048 || target.length > 4_096 || ids.has(id)) {
|
|
447
|
+
throw pptxSecurity(
|
|
448
|
+
"Duplicate or oversized OPC relationship",
|
|
449
|
+
"invalid-package",
|
|
450
|
+
relationshipsPart,
|
|
451
|
+
);
|
|
452
|
+
}
|
|
453
|
+
ids.add(id);
|
|
454
|
+
const targetMode = pptxXmlAttribute(child, "TargetMode") ?? "Internal";
|
|
455
|
+
if (targetMode !== "Internal") {
|
|
456
|
+
throw pptxSecurity(
|
|
457
|
+
"External OPC relationships are forbidden",
|
|
458
|
+
"external-relationship",
|
|
459
|
+
relationshipsPart,
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
if (/^(?:https?|ftp|file|data|javascript):/i.test(target)) {
|
|
463
|
+
throw pptxSecurity(
|
|
464
|
+
"External OPC relationship target is forbidden",
|
|
465
|
+
"external-relationship",
|
|
466
|
+
relationshipsPart,
|
|
467
|
+
);
|
|
468
|
+
}
|
|
469
|
+
const partName = resolvePartTarget(sourcePart, target, relationshipsPart);
|
|
470
|
+
const relationship = { id, type, target, sourcePart, partName };
|
|
471
|
+
validateRelationshipKind(context, relationship, relationshipsPart);
|
|
472
|
+
relationships.push(relationship);
|
|
473
|
+
}
|
|
474
|
+
context.relationships += relationships.length;
|
|
475
|
+
if (context.relationships > context.limits.relationships) {
|
|
476
|
+
throw pptxSecurity(
|
|
477
|
+
"PPTX exceeds its total relationship limit",
|
|
478
|
+
"limit-exceeded",
|
|
479
|
+
relationshipsPart,
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
context.relationshipsCache.set(relationshipsPart, relationships);
|
|
483
|
+
return relationships;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function validateRelationshipKind(
|
|
487
|
+
context: ImportContext,
|
|
488
|
+
relationship: Relationship,
|
|
489
|
+
relationshipsPart: string,
|
|
490
|
+
): void {
|
|
491
|
+
const suffix = relationship.type.slice(relationship.type.lastIndexOf("/") + 1);
|
|
492
|
+
const standardOfficeRelationship = relationship.type === `${OFFICE_RELATIONSHIP_BASE}${suffix}`;
|
|
493
|
+
const standardPackageRelationship = relationship.type === `${PACKAGE_RELATIONSHIP_BASE}${suffix}`;
|
|
494
|
+
if (
|
|
495
|
+
!standardOfficeRelationship &&
|
|
496
|
+
!standardPackageRelationship &&
|
|
497
|
+
!ALLOWED_EXACT_RELATIONSHIPS.has(relationship.type)
|
|
498
|
+
) {
|
|
499
|
+
throw pptxSecurity(
|
|
500
|
+
`Unsupported or non-standard presentation relationship type: ${relationship.type}`,
|
|
501
|
+
"invalid-package",
|
|
502
|
+
relationshipsPart,
|
|
503
|
+
);
|
|
504
|
+
}
|
|
505
|
+
if (ACTIVE_RELATIONSHIP_SUFFIX.test(suffix)) {
|
|
506
|
+
throw pptxSecurity(
|
|
507
|
+
"Active or remote presentation relationship is forbidden",
|
|
508
|
+
"active-content",
|
|
509
|
+
relationshipsPart,
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
if (!KNOWN_RELATIONSHIP_SUFFIXES.has(suffix)) {
|
|
513
|
+
markSourceOnly(context, [relationshipsPart, relationship.partName]);
|
|
514
|
+
}
|
|
515
|
+
if (suffix === "package") {
|
|
516
|
+
const safeChartWorkbook =
|
|
517
|
+
/^ppt\/charts\/chart\d+\.xml$/i.test(relationship.sourcePart) &&
|
|
518
|
+
/^ppt\/embeddings\/[^/]+\.xlsx$/i.test(relationship.partName);
|
|
519
|
+
if (!safeChartWorkbook) {
|
|
520
|
+
throw pptxSecurity(
|
|
521
|
+
"Embedded packages are forbidden outside chart data workbooks",
|
|
522
|
+
"active-content",
|
|
523
|
+
relationshipsPart,
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (suffix === "hyperlink") {
|
|
528
|
+
context.unsupportedParts.add(relationshipsPart);
|
|
529
|
+
context.issues.push(fidelityIssue("hyperlink", [relationshipsPart]));
|
|
530
|
+
}
|
|
531
|
+
if (SOURCE_ONLY_RELATIONSHIP_SUFFIXES.has(suffix)) {
|
|
532
|
+
context.unsupportedParts.add(relationshipsPart);
|
|
533
|
+
context.issues.push(fidelityIssue("theme", [relationshipsPart, relationship.partName]));
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function classifySourceOnlyParts(context: ImportContext): void {
|
|
538
|
+
for (const entry of context.entries.values()) {
|
|
539
|
+
if (!entry.directory && !isKnownPptxPart(entry.name)) {
|
|
540
|
+
markSourceOnly(context, [entry.name, "[Content_Types].xml"]);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function markSourceOnly(context: ImportContext, parts: readonly string[]): void {
|
|
546
|
+
for (const part of parts) context.unsupportedParts.add(part);
|
|
547
|
+
context.issues.push(fidelityIssue("source-only", parts));
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function isKnownPptxPart(name: string): boolean {
|
|
551
|
+
return KNOWN_PPTX_PART_PATTERNS.some((pattern) => pattern.test(name));
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function isActiveContentType(contentType: string): boolean {
|
|
555
|
+
return /(?:macroEnabled|vbaProject|activeX|oleObject|javascript|ecmascript|x-msdownload|executable|powershell|shellscript|text\/html)/i.test(
|
|
556
|
+
contentType,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
function validateRelationshipTargets(context: ImportContext): void {
|
|
561
|
+
for (const relationships of context.relationshipsCache.values()) {
|
|
562
|
+
for (const relationship of relationships) {
|
|
563
|
+
const suffix = relationship.type.slice(relationship.type.lastIndexOf("/") + 1);
|
|
564
|
+
if (suffix === "hyperlink") continue;
|
|
565
|
+
if (!context.entries.has(relationship.partName)) {
|
|
566
|
+
throw pptxSecurity(
|
|
567
|
+
"OPC relationship targets a missing package part",
|
|
568
|
+
"invalid-package",
|
|
569
|
+
relationship.partName,
|
|
570
|
+
);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
async function preflightEmbeddedXlsx(
|
|
577
|
+
context: ImportContext,
|
|
578
|
+
entry: BoundedZipEntry,
|
|
579
|
+
bytes: Uint8Array,
|
|
580
|
+
): Promise<void> {
|
|
581
|
+
if (entry.expandedSize > context.limits.nestedPackageBytes) {
|
|
582
|
+
throw pptxSecurity(
|
|
583
|
+
"Embedded chart workbook exceeds its byte limit",
|
|
584
|
+
"limit-exceeded",
|
|
585
|
+
entry.name,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
const nested = parseBoundedZip(
|
|
589
|
+
bytes,
|
|
590
|
+
{
|
|
591
|
+
entries: Math.min(context.limits.entries, 10_000),
|
|
592
|
+
compressedEntryBytes: context.limits.nestedPackageBytes,
|
|
593
|
+
expandedEntryBytes: context.limits.nestedPackageBytes,
|
|
594
|
+
expandedBytes: context.limits.nestedPackageBytes,
|
|
595
|
+
compressionRatio: context.limits.compressionRatio,
|
|
596
|
+
compressionRatioThreshold: 1_048_576,
|
|
597
|
+
},
|
|
598
|
+
pptxZipFailure,
|
|
599
|
+
);
|
|
600
|
+
context.nestedEntries += nested.length;
|
|
601
|
+
context.nestedExpandedBytes += nested.reduce((sum, part) => sum + part.expandedSize, 0);
|
|
602
|
+
if (
|
|
603
|
+
context.nestedEntries > context.limits.nestedEntries ||
|
|
604
|
+
context.nestedExpandedBytes > context.limits.nestedExpandedBytes
|
|
605
|
+
) {
|
|
606
|
+
throw pptxSecurity(
|
|
607
|
+
"Embedded chart workbooks exceed the global nested archive budget",
|
|
608
|
+
"limit-exceeded",
|
|
609
|
+
entry.name,
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
for (const nestedEntry of nested) {
|
|
613
|
+
rejectNestedWorkbookEntry(nestedEntry.name, entry.name);
|
|
614
|
+
if (nestedEntry.directory) continue;
|
|
615
|
+
if (isXmlPart(nestedEntry.name)) {
|
|
616
|
+
const xmlBytes = await inflateBoundedZipEntry(
|
|
617
|
+
bytes,
|
|
618
|
+
nestedEntry,
|
|
619
|
+
context.limits.nestedPackageBytes,
|
|
620
|
+
pptxZipFailure,
|
|
621
|
+
);
|
|
622
|
+
const root = parsePptxXmlPart(
|
|
623
|
+
xmlBytes,
|
|
624
|
+
`${entry.name}!/${nestedEntry.name}`,
|
|
625
|
+
context.xmlBudget,
|
|
626
|
+
);
|
|
627
|
+
if (nestedEntry.name.endsWith(".rels")) {
|
|
628
|
+
for (const relationship of pptxXmlChildren(root)) {
|
|
629
|
+
if (relationship.localName !== "Relationship") {
|
|
630
|
+
throw pptxSecurity(
|
|
631
|
+
"Embedded workbook relationship XML is invalid",
|
|
632
|
+
"invalid-package",
|
|
633
|
+
entry.name,
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
const targetMode = pptxXmlAttribute(relationship, "TargetMode") ?? "Internal";
|
|
637
|
+
const target = requiredAttribute(
|
|
638
|
+
relationship,
|
|
639
|
+
"Target",
|
|
640
|
+
`${entry.name}!/${nestedEntry.name}`,
|
|
641
|
+
);
|
|
642
|
+
if (
|
|
643
|
+
targetMode !== "Internal" ||
|
|
644
|
+
/^(?:https?|ftp|file|data|javascript):/i.test(target) ||
|
|
645
|
+
target.includes("\\") ||
|
|
646
|
+
/%(?:2e|2f|5c)/i.test(target)
|
|
647
|
+
) {
|
|
648
|
+
throw pptxSecurity(
|
|
649
|
+
"Embedded chart workbook contains an external relationship",
|
|
650
|
+
"external-relationship",
|
|
651
|
+
entry.name,
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
if (pptxXmlDescendants(root, "f").length > 0) {
|
|
657
|
+
throw pptxSecurity(
|
|
658
|
+
"Formula-bearing embedded chart workbooks are not retained",
|
|
659
|
+
"active-content",
|
|
660
|
+
entry.name,
|
|
661
|
+
);
|
|
662
|
+
}
|
|
663
|
+
} else {
|
|
664
|
+
await verifyBoundedZipEntry(
|
|
665
|
+
bytes,
|
|
666
|
+
nestedEntry,
|
|
667
|
+
context.limits.nestedPackageBytes,
|
|
668
|
+
pptxZipFailure,
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function rejectNestedWorkbookEntry(name: string, container: string): void {
|
|
675
|
+
if (
|
|
676
|
+
/(?:^|\/)(?:vbaProject|vbaData)\.bin$/i.test(name) ||
|
|
677
|
+
/(?:^|\/)(?:activeX|embeddings|externalLinks|connections|queryTables|webextensions)(?:\/|$)/i.test(
|
|
678
|
+
name,
|
|
679
|
+
) ||
|
|
680
|
+
/\.(?:exe|dll|com|msi|js|vbs|bat|cmd|ps1|sh|scr|jar)$/i.test(name)
|
|
681
|
+
) {
|
|
682
|
+
throw pptxSecurity(
|
|
683
|
+
`Embedded chart workbook contains active content: ${name}`,
|
|
684
|
+
"active-content",
|
|
685
|
+
container,
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function rejectActiveEntry(name: string, directory = false): void {
|
|
691
|
+
const safeChartWorkbook = /^ppt\/embeddings\/[^/]+\.xlsx$/i.test(name);
|
|
692
|
+
if (
|
|
693
|
+
/(?:^|\/)(?:vbaProject|vbaData)\.bin$/i.test(name) ||
|
|
694
|
+
/(?:^|\/)(?:activeX|oleObject|externalLinks|connections|webextensions)(?:\/|$)/i.test(name) ||
|
|
695
|
+
/^ppt\/(?:audio|video)\//i.test(name) ||
|
|
696
|
+
(/^ppt\/embeddings\//i.test(name) && !directory && !safeChartWorkbook) ||
|
|
697
|
+
(!directory && /\.(?:zip|docx|dotx|xlsx|xltx|pptx|ppsx)$/i.test(name) && !safeChartWorkbook) ||
|
|
698
|
+
/\.(?:exe|dll|com|msi|js|vbs|vbe|bat|cmd|ps1|sh|scr|jar|ppam|pptm|svg|svgz|html?|mhtml)$/i.test(
|
|
699
|
+
name,
|
|
700
|
+
)
|
|
701
|
+
) {
|
|
702
|
+
throw pptxSecurity("Active presentation content is forbidden", "active-content", name);
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
function isRasterMediaPart(name: string): boolean {
|
|
707
|
+
return /^ppt\/media\/[^/]+\.(?:png|jpe?g|gif|webp)$/i.test(name);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function reserveRetained(context: ImportContext, bytes: number, partName: string): void {
|
|
711
|
+
context.retainedBytes += bytes;
|
|
712
|
+
if (context.retainedBytes > context.limits.retainedBytes) {
|
|
713
|
+
throw pptxSecurity("PPTX exceeds its retained-memory budget", "limit-exceeded", partName);
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
function importMastersAndLayouts(
|
|
718
|
+
context: ImportContext,
|
|
719
|
+
presentation: Presentation,
|
|
720
|
+
presentationRoot: PptxXmlElement,
|
|
721
|
+
presentationRelationships: ReadonlyMap<string, Relationship>,
|
|
722
|
+
masterByPart: Map<string, PresentationMaster>,
|
|
723
|
+
layoutByPart: Map<string, PresentationLayout>,
|
|
724
|
+
): void {
|
|
725
|
+
const masterIds = pptxXmlChildren(pptxXmlChild(presentationRoot, "sldMasterIdLst"));
|
|
726
|
+
for (const masterId of masterIds) {
|
|
727
|
+
const relationshipId = requiredRelationshipId(masterId, "ppt/presentation.xml");
|
|
728
|
+
const relationship = presentationRelationships.get(relationshipId);
|
|
729
|
+
if (!relationship || relationship.type !== `${OFFICE_RELATIONSHIP_BASE}slideMaster`) {
|
|
730
|
+
throw pptxSecurity(
|
|
731
|
+
"Slide master id references the wrong relationship",
|
|
732
|
+
"invalid-package",
|
|
733
|
+
"ppt/presentation.xml",
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
const masterRoot = readXml(context, relationship.partName);
|
|
737
|
+
requireRoot(masterRoot, "sldMaster", PRESENTATION_NAMESPACE, relationship.partName);
|
|
738
|
+
const common = requiredChild(masterRoot, "cSld", relationship.partName);
|
|
739
|
+
const masterRelationships = byRelationshipId(relationshipsFor(context, relationship.partName));
|
|
740
|
+
const elements = parseTemplateTree(
|
|
741
|
+
context,
|
|
742
|
+
requiredChild(common, "spTree", relationship.partName),
|
|
743
|
+
masterRelationships,
|
|
744
|
+
relationship.partName,
|
|
745
|
+
);
|
|
746
|
+
const master = presentation.masters.add({
|
|
747
|
+
name: pptxXmlAttribute(common, "name") ?? `Master ${presentation.masters.items.length + 1}`,
|
|
748
|
+
background: parseBackground(context, common),
|
|
749
|
+
elements,
|
|
750
|
+
});
|
|
751
|
+
masterByPart.set(relationship.partName, master);
|
|
752
|
+
|
|
753
|
+
for (const layoutId of pptxXmlChildren(pptxXmlChild(masterRoot, "sldLayoutIdLst"))) {
|
|
754
|
+
const layoutRelationshipId = requiredRelationshipId(layoutId, relationship.partName);
|
|
755
|
+
const layoutRelationship = masterRelationships.get(layoutRelationshipId);
|
|
756
|
+
if (
|
|
757
|
+
!layoutRelationship ||
|
|
758
|
+
layoutRelationship.type !== `${OFFICE_RELATIONSHIP_BASE}slideLayout`
|
|
759
|
+
) {
|
|
760
|
+
throw pptxSecurity(
|
|
761
|
+
"Slide layout id references the wrong relationship",
|
|
762
|
+
"invalid-package",
|
|
763
|
+
relationship.partName,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
if (layoutByPart.has(layoutRelationship.partName)) continue;
|
|
767
|
+
const layoutRoot = readXml(context, layoutRelationship.partName);
|
|
768
|
+
requireRoot(layoutRoot, "sldLayout", PRESENTATION_NAMESPACE, layoutRelationship.partName);
|
|
769
|
+
const layoutCommon = requiredChild(layoutRoot, "cSld", layoutRelationship.partName);
|
|
770
|
+
const layoutRelationships = byRelationshipId(
|
|
771
|
+
relationshipsFor(context, layoutRelationship.partName),
|
|
772
|
+
);
|
|
773
|
+
const parent = [...layoutRelationships.values()].find(
|
|
774
|
+
(candidate) => candidate.type === `${OFFICE_RELATIONSHIP_BASE}slideMaster`,
|
|
775
|
+
);
|
|
776
|
+
if (!parent || parent.partName !== relationship.partName) {
|
|
777
|
+
throw pptxSecurity(
|
|
778
|
+
"Slide layout does not link back to its master",
|
|
779
|
+
"invalid-package",
|
|
780
|
+
layoutRelationship.partName,
|
|
781
|
+
);
|
|
782
|
+
}
|
|
783
|
+
const layout = presentation.layouts.add({
|
|
784
|
+
name:
|
|
785
|
+
pptxXmlAttribute(layoutCommon, "name") ??
|
|
786
|
+
`Layout ${presentation.layouts.items.length + 1}`,
|
|
787
|
+
masterId: master.id,
|
|
788
|
+
background: parseBackground(context, layoutCommon),
|
|
789
|
+
elements: parseTemplateTree(
|
|
790
|
+
context,
|
|
791
|
+
requiredChild(layoutCommon, "spTree", layoutRelationship.partName),
|
|
792
|
+
layoutRelationships,
|
|
793
|
+
layoutRelationship.partName,
|
|
794
|
+
),
|
|
795
|
+
});
|
|
796
|
+
layoutByPart.set(layoutRelationship.partName, layout);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
function importSlides(
|
|
802
|
+
context: ImportContext,
|
|
803
|
+
presentation: Presentation,
|
|
804
|
+
presentationRoot: PptxXmlElement,
|
|
805
|
+
presentationRelationships: ReadonlyMap<string, Relationship>,
|
|
806
|
+
layoutByPart: ReadonlyMap<string, PresentationLayout>,
|
|
807
|
+
): void {
|
|
808
|
+
const slideIds = pptxXmlChildren(pptxXmlChild(presentationRoot, "sldIdLst"));
|
|
809
|
+
if (slideIds.length > context.limits.slides) {
|
|
810
|
+
throw pptxSecurity("PPTX exceeds its slide limit", "limit-exceeded", "ppt/presentation.xml");
|
|
811
|
+
}
|
|
812
|
+
const seenParts = new Set<string>();
|
|
813
|
+
for (const slideId of slideIds) {
|
|
814
|
+
const relationshipId = requiredRelationshipId(slideId, "ppt/presentation.xml");
|
|
815
|
+
const relationship = presentationRelationships.get(relationshipId);
|
|
816
|
+
if (
|
|
817
|
+
!relationship ||
|
|
818
|
+
relationship.type !== `${OFFICE_RELATIONSHIP_BASE}slide` ||
|
|
819
|
+
seenParts.has(relationship.partName)
|
|
820
|
+
) {
|
|
821
|
+
throw pptxSecurity(
|
|
822
|
+
"Slide id references an invalid or duplicate slide",
|
|
823
|
+
"invalid-package",
|
|
824
|
+
"ppt/presentation.xml",
|
|
825
|
+
);
|
|
826
|
+
}
|
|
827
|
+
seenParts.add(relationship.partName);
|
|
828
|
+
const root = readXml(context, relationship.partName);
|
|
829
|
+
requireRoot(root, "sld", PRESENTATION_NAMESPACE, relationship.partName);
|
|
830
|
+
const common = requiredChild(root, "cSld", relationship.partName);
|
|
831
|
+
const relationships = relationshipsFor(context, relationship.partName);
|
|
832
|
+
const relationshipById = byRelationshipId(relationships);
|
|
833
|
+
const slide = presentation.slides.add();
|
|
834
|
+
slide.title = pptxXmlAttribute(common, "name") ?? "";
|
|
835
|
+
slide.background.fill = parseBackground(context, common);
|
|
836
|
+
const layoutRelationship = relationships.find(
|
|
837
|
+
(candidate) => candidate.type === `${OFFICE_RELATIONSHIP_BASE}slideLayout`,
|
|
838
|
+
);
|
|
839
|
+
if (!layoutRelationship) {
|
|
840
|
+
throw pptxSecurity(
|
|
841
|
+
"Slide lacks a slide-layout relationship",
|
|
842
|
+
"invalid-package",
|
|
843
|
+
relationship.partName,
|
|
844
|
+
);
|
|
845
|
+
}
|
|
846
|
+
const layout = layoutByPart.get(layoutRelationship.partName);
|
|
847
|
+
if (!layout) {
|
|
848
|
+
throw pptxSecurity(
|
|
849
|
+
"Slide references an unknown slide layout",
|
|
850
|
+
"invalid-package",
|
|
851
|
+
relationship.partName,
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
slide.setLayout(layout);
|
|
855
|
+
const parsed = parseSceneTree(
|
|
856
|
+
context,
|
|
857
|
+
requiredChild(common, "spTree", relationship.partName),
|
|
858
|
+
relationshipById,
|
|
859
|
+
relationship.partName,
|
|
860
|
+
);
|
|
861
|
+
let derivedTitle: string | undefined;
|
|
862
|
+
for (const element of parsed) {
|
|
863
|
+
switch (element.kind) {
|
|
864
|
+
case "shape": {
|
|
865
|
+
const shape = slide.shapes.add(element.config);
|
|
866
|
+
if (
|
|
867
|
+
!derivedTitle &&
|
|
868
|
+
(shape.placeholder?.type === "title" || shape.placeholder?.type === "ctrTitle") &&
|
|
869
|
+
shape.text.toString().length > 0
|
|
870
|
+
)
|
|
871
|
+
derivedTitle = shape.text.toString();
|
|
872
|
+
break;
|
|
873
|
+
}
|
|
874
|
+
case "chart":
|
|
875
|
+
slide.charts.add(element.type, element.config ?? {});
|
|
876
|
+
break;
|
|
877
|
+
case "image":
|
|
878
|
+
slide.images.add(element.config);
|
|
879
|
+
break;
|
|
880
|
+
case "table":
|
|
881
|
+
slide.tables.add(element.config);
|
|
882
|
+
break;
|
|
883
|
+
case "group":
|
|
884
|
+
slide.groups.add(element.config);
|
|
885
|
+
break;
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
if (derivedTitle) slide.title = derivedTitle;
|
|
889
|
+
const notesRelationship = relationships.find(
|
|
890
|
+
(candidate) => candidate.type === `${OFFICE_RELATIONSHIP_BASE}notesSlide`,
|
|
891
|
+
);
|
|
892
|
+
if (notesRelationship) slide.notes.set(parseNotes(context, notesRelationship.partName));
|
|
893
|
+
if (pptxXmlChild(root, "transition"))
|
|
894
|
+
addUnsupported(context, "transition", relationship.partName);
|
|
895
|
+
if (pptxXmlChild(root, "timing")) addUnsupported(context, "animation", relationship.partName);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function parseTemplateTree(
|
|
900
|
+
context: ImportContext,
|
|
901
|
+
tree: PptxXmlElement,
|
|
902
|
+
relationships: ReadonlyMap<string, Relationship>,
|
|
903
|
+
partName: string,
|
|
904
|
+
): PresentationTemplateElement[] {
|
|
905
|
+
const output: PresentationTemplateElement[] = [];
|
|
906
|
+
for (const element of parseSceneTree(context, tree, relationships, partName)) {
|
|
907
|
+
if (element.kind === "group") {
|
|
908
|
+
// The model keeps true groups on slides; template groups remain source-preserved
|
|
909
|
+
// until the package-native writer supports inherited nested group transforms.
|
|
910
|
+
addUnsupported(context, "group", partName);
|
|
911
|
+
} else output.push(element);
|
|
912
|
+
}
|
|
913
|
+
return output;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function parseSceneTree(
|
|
917
|
+
context: ImportContext,
|
|
918
|
+
tree: PptxXmlElement,
|
|
919
|
+
relationships: ReadonlyMap<string, Relationship>,
|
|
920
|
+
partName: string,
|
|
921
|
+
transform: Transform = IDENTITY_TRANSFORM,
|
|
922
|
+
depth = 0,
|
|
923
|
+
): ParsedElement[] {
|
|
924
|
+
if (depth > 32)
|
|
925
|
+
throw pptxSecurity("Presentation group nesting exceeds its limit", "limit-exceeded", partName);
|
|
926
|
+
const output: ParsedElement[] = [];
|
|
927
|
+
for (const element of pptxXmlChildren(tree)) {
|
|
928
|
+
if (element.localName === "nvGrpSpPr" || element.localName === "grpSpPr") continue;
|
|
929
|
+
consumeElement(context, partName);
|
|
930
|
+
switch (element.localName) {
|
|
931
|
+
case "sp":
|
|
932
|
+
case "cxnSp":
|
|
933
|
+
if (element.localName === "cxnSp") {
|
|
934
|
+
addUnsupported(context, "connector", partName, "style-approximated");
|
|
935
|
+
}
|
|
936
|
+
output.push({ kind: "shape", config: parseShape(context, element, partName, transform) });
|
|
937
|
+
break;
|
|
938
|
+
case "pic":
|
|
939
|
+
{
|
|
940
|
+
const image = parseImage(context, element, relationships, partName, transform);
|
|
941
|
+
if (image) output.push({ kind: "image", config: image });
|
|
942
|
+
}
|
|
943
|
+
break;
|
|
944
|
+
case "graphicFrame": {
|
|
945
|
+
const graphicData = pptxXmlDescendants(element, "graphicData")[0];
|
|
946
|
+
const uri = pptxXmlAttribute(graphicData, "uri");
|
|
947
|
+
if (uri === TABLE_GRAPHIC_URI) {
|
|
948
|
+
output.push({ kind: "table", config: parseTable(context, element, partName, transform) });
|
|
949
|
+
} else if (uri === CHART_GRAPHIC_URI) {
|
|
950
|
+
const chartRef = pptxXmlDescendants(graphicData, "chart")[0];
|
|
951
|
+
const relationshipId = requiredRelationshipId(chartRef, partName);
|
|
952
|
+
const relationship = relationships.get(relationshipId);
|
|
953
|
+
if (!relationship || relationship.type !== `${OFFICE_RELATIONSHIP_BASE}chart`) {
|
|
954
|
+
throw pptxSecurity(
|
|
955
|
+
"Chart frame references the wrong relationship",
|
|
956
|
+
"invalid-package",
|
|
957
|
+
partName,
|
|
958
|
+
);
|
|
959
|
+
}
|
|
960
|
+
const parsed = parseChart(context, relationship.partName);
|
|
961
|
+
if (parsed) {
|
|
962
|
+
const frameName = nonVisualName(element);
|
|
963
|
+
if (frameName) parsed.config.name = frameName;
|
|
964
|
+
parsed.config.position = parseFramePosition(element, partName, transform);
|
|
965
|
+
output.push({ kind: "chart", ...parsed });
|
|
966
|
+
}
|
|
967
|
+
} else {
|
|
968
|
+
addUnsupported(context, "smart-art", partName);
|
|
969
|
+
}
|
|
970
|
+
break;
|
|
971
|
+
}
|
|
972
|
+
case "grpSp": {
|
|
973
|
+
const group = parseGroup(context, element, relationships, partName, transform, depth + 1);
|
|
974
|
+
output.push({ kind: "group", config: group });
|
|
975
|
+
break;
|
|
976
|
+
}
|
|
977
|
+
case "contentPart":
|
|
978
|
+
case "alternateContent":
|
|
979
|
+
case "extLst":
|
|
980
|
+
addUnsupported(context, "custom-geometry", partName);
|
|
981
|
+
break;
|
|
982
|
+
default:
|
|
983
|
+
addUnsupported(context, "custom-geometry", partName);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return output;
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
function parseGroup(
|
|
990
|
+
context: ImportContext,
|
|
991
|
+
group: PptxXmlElement,
|
|
992
|
+
relationships: ReadonlyMap<string, Relationship>,
|
|
993
|
+
partName: string,
|
|
994
|
+
parentTransform: Transform,
|
|
995
|
+
depth: number,
|
|
996
|
+
): PresentationGroupConfig {
|
|
997
|
+
const properties = requiredChild(group, "grpSpPr", partName);
|
|
998
|
+
const xfrm = requiredChild(properties, "xfrm", partName);
|
|
999
|
+
const rawPosition = rawPositionFromXfrm(xfrm, partName);
|
|
1000
|
+
const childOffsetElement = requiredChild(xfrm, "chOff", partName);
|
|
1001
|
+
const childExtentElement = requiredChild(xfrm, "chExt", partName);
|
|
1002
|
+
const childOffset = {
|
|
1003
|
+
left: emuNumber(requiredAttribute(childOffsetElement, "x", partName), partName),
|
|
1004
|
+
top: emuNumber(requiredAttribute(childOffsetElement, "y", partName), partName),
|
|
1005
|
+
};
|
|
1006
|
+
const childExtent = {
|
|
1007
|
+
width: positiveEmu(requiredAttribute(childExtentElement, "cx", partName), partName),
|
|
1008
|
+
height: positiveEmu(requiredAttribute(childExtentElement, "cy", partName), partName),
|
|
1009
|
+
};
|
|
1010
|
+
const position = applyTransform(rawPosition, parentTransform);
|
|
1011
|
+
const name = nonVisualName(group) ?? `Group ${context.elements}`;
|
|
1012
|
+
return {
|
|
1013
|
+
name,
|
|
1014
|
+
position,
|
|
1015
|
+
childOffset,
|
|
1016
|
+
childExtent,
|
|
1017
|
+
rotation: rotationFromXfrm(xfrm, partName),
|
|
1018
|
+
flipHorizontal: booleanAttribute(xfrm, "flipH"),
|
|
1019
|
+
flipVertical: booleanAttribute(xfrm, "flipV"),
|
|
1020
|
+
children: parseSceneTree(context, group, relationships, partName, IDENTITY_TRANSFORM, depth),
|
|
1021
|
+
};
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function parseShape(
|
|
1025
|
+
context: ImportContext,
|
|
1026
|
+
element: PptxXmlElement,
|
|
1027
|
+
partName: string,
|
|
1028
|
+
transform: Transform,
|
|
1029
|
+
): PresentationShapeConfig {
|
|
1030
|
+
const properties = pptxXmlChild(element, "spPr");
|
|
1031
|
+
const xfrm = pptxXmlChild(properties, "xfrm");
|
|
1032
|
+
const position = xfrm
|
|
1033
|
+
? applyTransform(rawPositionFromXfrm(xfrm, partName), transform)
|
|
1034
|
+
: applyTransform({ left: 0, top: 0, width: 1, height: 1 }, transform);
|
|
1035
|
+
const preset =
|
|
1036
|
+
pptxXmlAttribute(pptxXmlChild(properties, "prstGeom"), "prst") ??
|
|
1037
|
+
(element.localName === "cxnSp" ? "line" : "rect");
|
|
1038
|
+
const geometry = mapGeometry(context, preset, partName);
|
|
1039
|
+
const text = parseTextBody(context, pptxXmlChild(element, "txBody"), partName);
|
|
1040
|
+
const placeholderElement = pptxXmlDescendants(
|
|
1041
|
+
pptxXmlChild(element, element.localName === "cxnSp" ? "nvCxnSpPr" : "nvSpPr"),
|
|
1042
|
+
"ph",
|
|
1043
|
+
)[0];
|
|
1044
|
+
const placeholderType = pptxXmlAttribute(placeholderElement, "type");
|
|
1045
|
+
const placeholderIndex = optionalInteger(pptxXmlAttribute(placeholderElement, "idx"), partName);
|
|
1046
|
+
return {
|
|
1047
|
+
geometry,
|
|
1048
|
+
name: nonVisualName(element) ?? `Shape ${context.elements}`,
|
|
1049
|
+
position,
|
|
1050
|
+
fill: parseFill(context, properties, "white") ?? "white",
|
|
1051
|
+
line: parseLine(context, pptxXmlChild(properties, "ln"), partName),
|
|
1052
|
+
text: text.value,
|
|
1053
|
+
textStyle: text.style,
|
|
1054
|
+
rotation: xfrm ? rotationFromXfrm(xfrm, partName) : 0,
|
|
1055
|
+
...(placeholderType
|
|
1056
|
+
? {
|
|
1057
|
+
placeholder: {
|
|
1058
|
+
type: placeholderType,
|
|
1059
|
+
...(placeholderIndex === undefined ? {} : { index: placeholderIndex }),
|
|
1060
|
+
},
|
|
1061
|
+
}
|
|
1062
|
+
: {}),
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
|
|
1066
|
+
function parseImage(
|
|
1067
|
+
context: ImportContext,
|
|
1068
|
+
element: PptxXmlElement,
|
|
1069
|
+
relationships: ReadonlyMap<string, Relationship>,
|
|
1070
|
+
partName: string,
|
|
1071
|
+
transform: Transform,
|
|
1072
|
+
): PresentationImageConfig | undefined {
|
|
1073
|
+
const blip = pptxXmlDescendants(element, "blip")[0];
|
|
1074
|
+
const relationshipId = requiredRelationshipId(blip, partName, "embed");
|
|
1075
|
+
const relationship = relationships.get(relationshipId);
|
|
1076
|
+
if (!relationship || relationship.type !== `${OFFICE_RELATIONSHIP_BASE}image`) {
|
|
1077
|
+
throw pptxSecurity("Picture references the wrong relationship", "invalid-package", partName);
|
|
1078
|
+
}
|
|
1079
|
+
if (!isRasterMediaPart(relationship.partName)) {
|
|
1080
|
+
addUnsupported(context, "media", relationship.partName);
|
|
1081
|
+
return undefined;
|
|
1082
|
+
}
|
|
1083
|
+
const bytes = readPartBytes(context, relationship.partName);
|
|
1084
|
+
context.imageBytes += bytes.byteLength;
|
|
1085
|
+
if (context.imageBytes > context.limits.imageBytes) {
|
|
1086
|
+
throw pptxSecurity(
|
|
1087
|
+
"PPTX exceeds its decoded image byte limit",
|
|
1088
|
+
"limit-exceeded",
|
|
1089
|
+
relationship.partName,
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
const properties = requiredChild(element, "spPr", partName);
|
|
1093
|
+
const xfrm = requiredChild(properties, "xfrm", partName);
|
|
1094
|
+
const cropElement = pptxXmlDescendants(pptxXmlChild(element, "blipFill"), "srcRect")[0];
|
|
1095
|
+
const crop = cropElement
|
|
1096
|
+
? {
|
|
1097
|
+
left: cropPercent(pptxXmlAttribute(cropElement, "l"), partName),
|
|
1098
|
+
top: cropPercent(pptxXmlAttribute(cropElement, "t"), partName),
|
|
1099
|
+
right: cropPercent(pptxXmlAttribute(cropElement, "r"), partName),
|
|
1100
|
+
bottom: cropPercent(pptxXmlAttribute(cropElement, "b"), partName),
|
|
1101
|
+
}
|
|
1102
|
+
: undefined;
|
|
1103
|
+
const geometry = mapImageGeometry(
|
|
1104
|
+
pptxXmlAttribute(pptxXmlChild(properties, "prstGeom"), "prst") ?? "rect",
|
|
1105
|
+
);
|
|
1106
|
+
const name = nonVisualName(element) ?? `Image ${context.elements}`;
|
|
1107
|
+
const description = nonVisualDescription(element) ?? "";
|
|
1108
|
+
const imageConfig: PresentationImageConfig = {
|
|
1109
|
+
blob: bytes.slice(),
|
|
1110
|
+
contentType: rasterContentType(relationship.partName),
|
|
1111
|
+
name,
|
|
1112
|
+
alt: description,
|
|
1113
|
+
position: applyTransform(rawPositionFromXfrm(xfrm, partName), transform),
|
|
1114
|
+
fit: crop ? "cover" : "contain",
|
|
1115
|
+
geometry,
|
|
1116
|
+
rotation: rotationFromXfrm(xfrm, partName),
|
|
1117
|
+
flipHorizontal: booleanAttribute(xfrm, "flipH"),
|
|
1118
|
+
flipVertical: booleanAttribute(xfrm, "flipV"),
|
|
1119
|
+
lockAspectRatio: booleanAttribute(pptxXmlDescendants(element, "picLocks")[0], "noChangeAspect"),
|
|
1120
|
+
...(crop ? { crop } : {}),
|
|
1121
|
+
};
|
|
1122
|
+
return imageConfig;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
function parseTable(
|
|
1126
|
+
context: ImportContext,
|
|
1127
|
+
frame: PptxXmlElement,
|
|
1128
|
+
partName: string,
|
|
1129
|
+
transform: Transform,
|
|
1130
|
+
): PresentationTableConfig {
|
|
1131
|
+
const table = pptxXmlDescendants(frame, "tbl")[0];
|
|
1132
|
+
if (!table) throw pptxSecurity("Table frame lacks table data", "invalid-package", partName);
|
|
1133
|
+
const grid = requiredChild(table, "tblGrid", partName);
|
|
1134
|
+
const columnWidths = pptxXmlChildren(grid).map((column) =>
|
|
1135
|
+
positiveEmu(requiredAttribute(column, "w", partName), partName),
|
|
1136
|
+
);
|
|
1137
|
+
if (columnWidths.length === 0) {
|
|
1138
|
+
throw pptxSecurity("Presentation table has no columns", "invalid-package", partName);
|
|
1139
|
+
}
|
|
1140
|
+
const rowElements = pptxXmlChildren(table).filter((child) => child.localName === "tr");
|
|
1141
|
+
const rows: PresentationTableCellInput[][] = [];
|
|
1142
|
+
const rowHeights: number[] = [];
|
|
1143
|
+
const occupied = Array.from(
|
|
1144
|
+
{ length: rowElements.length },
|
|
1145
|
+
() => new Uint8Array(columnWidths.length),
|
|
1146
|
+
);
|
|
1147
|
+
for (const [rowIndex, rowElement] of rowElements.entries()) {
|
|
1148
|
+
rowHeights.push(positiveEmu(requiredAttribute(rowElement, "h", partName), partName));
|
|
1149
|
+
const row: PresentationTableCellInput[] = Array.from(
|
|
1150
|
+
{ length: columnWidths.length },
|
|
1151
|
+
() => null,
|
|
1152
|
+
);
|
|
1153
|
+
let column = 0;
|
|
1154
|
+
for (const cellElement of pptxXmlChildren(rowElement).filter(
|
|
1155
|
+
(child) => child.localName === "tc",
|
|
1156
|
+
)) {
|
|
1157
|
+
if (column >= columnWidths.length) {
|
|
1158
|
+
throw pptxSecurity("Presentation table row exceeds its grid", "invalid-package", partName);
|
|
1159
|
+
}
|
|
1160
|
+
const properties = pptxXmlChild(cellElement, "tcPr");
|
|
1161
|
+
const horizontalMerge =
|
|
1162
|
+
booleanAttribute(cellElement, "hMerge") || booleanAttribute(properties, "hMerge");
|
|
1163
|
+
const verticalMerge =
|
|
1164
|
+
booleanAttribute(cellElement, "vMerge") || booleanAttribute(properties, "vMerge");
|
|
1165
|
+
if (horizontalMerge || verticalMerge) {
|
|
1166
|
+
// Covered continuation slots are represented as null. The anchor's span
|
|
1167
|
+
// attributes carry semantic occupancy.
|
|
1168
|
+
if (occupied[rowIndex]![column] === 0) {
|
|
1169
|
+
throw pptxSecurity(
|
|
1170
|
+
"Presentation table merge continuation has no spanning anchor",
|
|
1171
|
+
"invalid-package",
|
|
1172
|
+
partName,
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
column += 1;
|
|
1176
|
+
continue;
|
|
1177
|
+
}
|
|
1178
|
+
if (occupied[rowIndex]![column] !== 0) {
|
|
1179
|
+
throw pptxSecurity("Presentation table spans overlap", "invalid-package", partName);
|
|
1180
|
+
}
|
|
1181
|
+
const colSpan =
|
|
1182
|
+
optionalPositiveInteger(
|
|
1183
|
+
pptxXmlAttribute(cellElement, "gridSpan") ?? pptxXmlAttribute(properties, "gridSpan"),
|
|
1184
|
+
partName,
|
|
1185
|
+
) ?? 1;
|
|
1186
|
+
const rowSpan =
|
|
1187
|
+
optionalPositiveInteger(
|
|
1188
|
+
pptxXmlAttribute(cellElement, "rowSpan") ?? pptxXmlAttribute(properties, "rowSpan"),
|
|
1189
|
+
partName,
|
|
1190
|
+
) ?? 1;
|
|
1191
|
+
if (column + colSpan > columnWidths.length || rowIndex + rowSpan > rowElements.length) {
|
|
1192
|
+
throw pptxSecurity(
|
|
1193
|
+
"Presentation table cell span exceeds its grid",
|
|
1194
|
+
"invalid-package",
|
|
1195
|
+
partName,
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
const text = parseTextBody(context, pptxXmlChild(cellElement, "txBody"), partName);
|
|
1199
|
+
row[column] = {
|
|
1200
|
+
text: text.value,
|
|
1201
|
+
textStyle: text.style,
|
|
1202
|
+
fill: parseFill(context, properties, "white") ?? "white",
|
|
1203
|
+
colSpan,
|
|
1204
|
+
rowSpan,
|
|
1205
|
+
};
|
|
1206
|
+
for (let occupiedRow = rowIndex; occupiedRow < rowIndex + rowSpan; occupiedRow += 1) {
|
|
1207
|
+
for (let occupiedColumn = column; occupiedColumn < column + colSpan; occupiedColumn += 1) {
|
|
1208
|
+
if (occupiedRow === rowIndex && occupiedColumn === column) continue;
|
|
1209
|
+
if (occupied[occupiedRow]![occupiedColumn] !== 0) {
|
|
1210
|
+
throw pptxSecurity("Presentation table spans overlap", "invalid-package", partName);
|
|
1211
|
+
}
|
|
1212
|
+
occupied[occupiedRow]![occupiedColumn] = 1;
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
// DrawingML retains one physical <a:tc> per grid slot. A spanning anchor
|
|
1216
|
+
// is followed by hMerge/vMerge continuation cells, so advance by one
|
|
1217
|
+
// physical slot rather than by the semantic span.
|
|
1218
|
+
column += 1;
|
|
1219
|
+
}
|
|
1220
|
+
if (column !== columnWidths.length) {
|
|
1221
|
+
throw pptxSecurity(
|
|
1222
|
+
"Presentation table row does not cover its grid",
|
|
1223
|
+
"invalid-package",
|
|
1224
|
+
partName,
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
rows.push(row);
|
|
1228
|
+
}
|
|
1229
|
+
for (let row = 0; row < rows.length; row += 1) {
|
|
1230
|
+
for (let column = 0; column < columnWidths.length; column += 1) {
|
|
1231
|
+
if (rows[row]![column] === null && occupied[row]![column] === 0) {
|
|
1232
|
+
throw pptxSecurity(
|
|
1233
|
+
"Presentation table has an uncovered grid slot",
|
|
1234
|
+
"invalid-package",
|
|
1235
|
+
partName,
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
if (pptxXmlChild(table, "tblPr")) {
|
|
1241
|
+
const styleId = pptxXmlText(pptxXmlChild(pptxXmlChild(table, "tblPr"), "tableStyleId"));
|
|
1242
|
+
if (styleId) addUnsupported(context, "table-style", partName);
|
|
1243
|
+
}
|
|
1244
|
+
return {
|
|
1245
|
+
name: nonVisualName(frame) ?? `Table ${context.elements}`,
|
|
1246
|
+
position: parseFramePosition(frame, partName, transform),
|
|
1247
|
+
rows,
|
|
1248
|
+
columnWidths,
|
|
1249
|
+
rowHeights,
|
|
1250
|
+
};
|
|
1251
|
+
}
|
|
1252
|
+
|
|
1253
|
+
function parseChart(
|
|
1254
|
+
context: ImportContext,
|
|
1255
|
+
partName: string,
|
|
1256
|
+
): { type: PresentationChartType; config: PresentationChartConfig } | undefined {
|
|
1257
|
+
const root = readXml(context, partName);
|
|
1258
|
+
requireRoot(root, "chartSpace", CHART_NAMESPACE, partName);
|
|
1259
|
+
const chart = requiredChild(root, "chart", partName);
|
|
1260
|
+
const plot = requiredChild(chart, "plotArea", partName);
|
|
1261
|
+
const chartElement = pptxXmlChildren(plot).find((candidate) =>
|
|
1262
|
+
CHART_ELEMENT_TYPES.has(candidate.localName),
|
|
1263
|
+
);
|
|
1264
|
+
if (!chartElement) {
|
|
1265
|
+
addUnsupported(context, "unsupported-chart", partName);
|
|
1266
|
+
return undefined;
|
|
1267
|
+
}
|
|
1268
|
+
const type = CHART_ELEMENT_TYPES.get(chartElement.localName)!;
|
|
1269
|
+
if (chartElement.localName.includes("3D")) {
|
|
1270
|
+
addUnsupported(context, "unsupported-chart", partName, "style-approximated");
|
|
1271
|
+
}
|
|
1272
|
+
const series: PresentationChartSeriesConfig[] = [];
|
|
1273
|
+
for (const seriesElement of pptxXmlChildren(chartElement).filter(
|
|
1274
|
+
(candidate) => candidate.localName === "ser",
|
|
1275
|
+
)) {
|
|
1276
|
+
const name =
|
|
1277
|
+
parseChartString(pptxXmlChild(seriesElement, "tx")) || `Series ${series.length + 1}`;
|
|
1278
|
+
const categories = parseChartCategories(pptxXmlChild(seriesElement, "cat"));
|
|
1279
|
+
const values = parseChartNumbers(
|
|
1280
|
+
pptxXmlChild(seriesElement, type === "scatter" || type === "bubble" ? "yVal" : "val"),
|
|
1281
|
+
context,
|
|
1282
|
+
partName,
|
|
1283
|
+
);
|
|
1284
|
+
const xValues = parseChartNumbers(pptxXmlChild(seriesElement, "xVal"), context, partName);
|
|
1285
|
+
const bubbleSizes = parseChartNumbers(
|
|
1286
|
+
pptxXmlChild(seriesElement, "bubbleSize"),
|
|
1287
|
+
context,
|
|
1288
|
+
partName,
|
|
1289
|
+
);
|
|
1290
|
+
const fill = parseFill(context, pptxXmlChild(seriesElement, "spPr"), undefined);
|
|
1291
|
+
series.push({
|
|
1292
|
+
name,
|
|
1293
|
+
categories,
|
|
1294
|
+
values,
|
|
1295
|
+
...(xValues.length > 0 ? { xValues } : {}),
|
|
1296
|
+
...(bubbleSizes.length > 0 ? { bubbleSizes } : {}),
|
|
1297
|
+
...(fill ? { fill } : {}),
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
1300
|
+
const categories =
|
|
1301
|
+
series.find((candidate) => (candidate.categories?.length ?? 0) > 0)?.categories ?? [];
|
|
1302
|
+
const legend = pptxXmlChild(chart, "legend");
|
|
1303
|
+
const labels = pptxXmlChild(chartElement, "dLbls");
|
|
1304
|
+
const axes = pptxXmlChildren(plot).filter((candidate) => candidate.localName.endsWith("Ax"));
|
|
1305
|
+
const categoryAxis = axes.find(
|
|
1306
|
+
(axis) => axis.localName === "catAx" || axis.localName === "dateAx",
|
|
1307
|
+
);
|
|
1308
|
+
const valueAxis = axes.find((axis) => axis.localName === "valAx");
|
|
1309
|
+
return {
|
|
1310
|
+
type,
|
|
1311
|
+
config: {
|
|
1312
|
+
name: `Chart ${context.elements}`,
|
|
1313
|
+
title: parseChartString(pptxXmlChild(chart, "title")),
|
|
1314
|
+
categories,
|
|
1315
|
+
series,
|
|
1316
|
+
hasLegend: Boolean(legend) && !booleanAttribute(pptxXmlChild(legend, "delete"), "val"),
|
|
1317
|
+
...(legend
|
|
1318
|
+
? {
|
|
1319
|
+
legend: {
|
|
1320
|
+
position: chartLegendPosition(
|
|
1321
|
+
pptxXmlAttribute(pptxXmlChild(legend, "legendPos"), "val"),
|
|
1322
|
+
),
|
|
1323
|
+
},
|
|
1324
|
+
}
|
|
1325
|
+
: {}),
|
|
1326
|
+
...(parseChartAxis(categoryAxis) ? { xAxis: parseChartAxis(categoryAxis)! } : {}),
|
|
1327
|
+
...(parseChartAxis(valueAxis) ? { yAxis: parseChartAxis(valueAxis)! } : {}),
|
|
1328
|
+
dataLabels: {
|
|
1329
|
+
showValue: booleanAttribute(pptxXmlChild(labels, "showVal"), "val"),
|
|
1330
|
+
showSeriesName: booleanAttribute(pptxXmlChild(labels, "showSerName"), "val"),
|
|
1331
|
+
showCategoryName: booleanAttribute(pptxXmlChild(labels, "showCatName"), "val"),
|
|
1332
|
+
showPercent: booleanAttribute(pptxXmlChild(labels, "showPercent"), "val"),
|
|
1333
|
+
},
|
|
1334
|
+
},
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
function parseNotes(context: ImportContext, partName: string): string {
|
|
1339
|
+
const root = readXml(context, partName);
|
|
1340
|
+
requireRoot(root, "notes", PRESENTATION_NAMESPACE, partName);
|
|
1341
|
+
const common = requiredChild(root, "cSld", partName);
|
|
1342
|
+
const tree = requiredChild(common, "spTree", partName);
|
|
1343
|
+
const paragraphs: string[] = [];
|
|
1344
|
+
for (const shape of pptxXmlChildren(tree).filter((child) => child.localName === "sp")) {
|
|
1345
|
+
const placeholder = pptxXmlDescendants(shape, "ph")[0];
|
|
1346
|
+
const type = pptxXmlAttribute(placeholder, "type");
|
|
1347
|
+
if (type !== "body") continue;
|
|
1348
|
+
const text = parseTextBody(context, pptxXmlChild(shape, "txBody"), partName).value;
|
|
1349
|
+
if (text.length > 0) paragraphs.push(text);
|
|
1350
|
+
}
|
|
1351
|
+
return paragraphs.join("\n");
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
function parseTextBody(
|
|
1355
|
+
context: ImportContext,
|
|
1356
|
+
body: PptxXmlElement | undefined,
|
|
1357
|
+
partName: string,
|
|
1358
|
+
): { value: string; style: PresentationTextStyle } {
|
|
1359
|
+
if (!body) return { value: "", style: {} };
|
|
1360
|
+
const paragraphs: string[] = [];
|
|
1361
|
+
const styles: PresentationTextStyle[] = [];
|
|
1362
|
+
let paragraphAlignment: PresentationTextStyle["alignment"];
|
|
1363
|
+
for (const paragraph of pptxXmlChildren(body).filter((child) => child.localName === "p")) {
|
|
1364
|
+
const parts: string[] = [];
|
|
1365
|
+
const paragraphProperties = pptxXmlChild(paragraph, "pPr");
|
|
1366
|
+
const alignment = mapTextAlignment(pptxXmlAttribute(paragraphProperties, "algn"));
|
|
1367
|
+
if (alignment && !paragraphAlignment) paragraphAlignment = alignment;
|
|
1368
|
+
for (const child of pptxXmlChildren(paragraph)) {
|
|
1369
|
+
if (child.localName === "r" || child.localName === "fld") {
|
|
1370
|
+
const text = pptxXmlDirectText(pptxXmlChild(child, "t"));
|
|
1371
|
+
consumeText(context, text.length, partName);
|
|
1372
|
+
parts.push(text);
|
|
1373
|
+
const properties = pptxXmlChild(child, "rPr");
|
|
1374
|
+
if (properties) styles.push(parseTextStyle(context, properties, body));
|
|
1375
|
+
} else if (child.localName === "br") parts.push("\n");
|
|
1376
|
+
}
|
|
1377
|
+
paragraphs.push(parts.join(""));
|
|
1378
|
+
}
|
|
1379
|
+
const value = paragraphs.join("\n");
|
|
1380
|
+
const firstStyle = styles[0] ?? {};
|
|
1381
|
+
if (styles.some((style) => JSON.stringify(style) !== JSON.stringify(firstStyle))) {
|
|
1382
|
+
addUnsupported(context, "theme", partName, "style-approximated");
|
|
1383
|
+
}
|
|
1384
|
+
const verticalAlignment = mapVerticalAlignment(
|
|
1385
|
+
pptxXmlAttribute(pptxXmlChild(body, "bodyPr"), "anchor"),
|
|
1386
|
+
);
|
|
1387
|
+
return {
|
|
1388
|
+
value,
|
|
1389
|
+
style: {
|
|
1390
|
+
...firstStyle,
|
|
1391
|
+
...(paragraphAlignment ? { alignment: paragraphAlignment } : {}),
|
|
1392
|
+
...(verticalAlignment ? { verticalAlignment } : {}),
|
|
1393
|
+
},
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function parseTextStyle(
|
|
1398
|
+
context: ImportContext,
|
|
1399
|
+
properties: PptxXmlElement,
|
|
1400
|
+
body: PptxXmlElement,
|
|
1401
|
+
): PresentationTextStyle {
|
|
1402
|
+
const size = optionalNumber(pptxXmlAttribute(properties, "sz"));
|
|
1403
|
+
const font = pptxXmlAttribute(pptxXmlChild(properties, "latin"), "typeface");
|
|
1404
|
+
const fill = parseFill(context, properties, undefined);
|
|
1405
|
+
const underline = pptxXmlAttribute(properties, "u");
|
|
1406
|
+
const verticalAlignment = mapVerticalAlignment(
|
|
1407
|
+
pptxXmlAttribute(pptxXmlChild(body, "bodyPr"), "anchor"),
|
|
1408
|
+
);
|
|
1409
|
+
return {
|
|
1410
|
+
...(font && !font.startsWith("+") ? { fontFamily: font } : {}),
|
|
1411
|
+
...(size !== undefined ? { fontSize: size / 75 } : {}),
|
|
1412
|
+
bold: booleanAttribute(properties, "b"),
|
|
1413
|
+
italic: booleanAttribute(properties, "i"),
|
|
1414
|
+
underline: underline !== undefined && underline !== "none",
|
|
1415
|
+
...(fill && fill !== "none" ? { color: typeof fill === "string" ? fill : fill.color } : {}),
|
|
1416
|
+
...(verticalAlignment ? { verticalAlignment } : {}),
|
|
1417
|
+
};
|
|
1418
|
+
}
|
|
1419
|
+
|
|
1420
|
+
function parseBackground(context: ImportContext, common: PptxXmlElement): PresentationFill {
|
|
1421
|
+
const background = pptxXmlChild(common, "bg");
|
|
1422
|
+
if (!background) return "white";
|
|
1423
|
+
const properties = pptxXmlChild(background, "bgPr");
|
|
1424
|
+
if (properties) return parseFill(context, properties, "white") ?? "white";
|
|
1425
|
+
const reference = pptxXmlChild(background, "bgRef");
|
|
1426
|
+
const scheme = pptxXmlAttribute(pptxXmlChild(reference, "schemeClr"), "val");
|
|
1427
|
+
return scheme ? (context.themeColors.get(scheme) ?? "white") : "white";
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
function parseFill(
|
|
1431
|
+
context: ImportContext,
|
|
1432
|
+
parent: PptxXmlElement | undefined,
|
|
1433
|
+
fallback: PresentationFill | undefined,
|
|
1434
|
+
): PresentationFill | undefined {
|
|
1435
|
+
if (!parent) return fallback;
|
|
1436
|
+
if (pptxXmlChild(parent, "noFill")) return "none";
|
|
1437
|
+
const solid = pptxXmlChild(parent, "solidFill");
|
|
1438
|
+
if (!solid) return fallback;
|
|
1439
|
+
const srgb = pptxXmlChild(solid, "srgbClr");
|
|
1440
|
+
const scheme = pptxXmlChild(solid, "schemeClr");
|
|
1441
|
+
const system = pptxXmlChild(solid, "sysClr");
|
|
1442
|
+
const alpha = optionalNumber(
|
|
1443
|
+
pptxXmlAttribute(pptxXmlChild(srgb ?? scheme ?? system, "alpha"), "val"),
|
|
1444
|
+
);
|
|
1445
|
+
if (alpha === 0) return "none";
|
|
1446
|
+
const srgbValue = pptxXmlAttribute(srgb, "val");
|
|
1447
|
+
if (srgbValue && /^[0-9A-Fa-f]{6}$/.test(srgbValue)) return `#${srgbValue.toLowerCase()}`;
|
|
1448
|
+
const schemeValue = pptxXmlAttribute(scheme, "val");
|
|
1449
|
+
if (schemeValue) return context.themeColors.get(schemeValue) ?? fallback ?? "#000000";
|
|
1450
|
+
const systemValue = pptxXmlAttribute(system, "lastClr");
|
|
1451
|
+
if (systemValue && /^[0-9A-Fa-f]{6}$/.test(systemValue)) return `#${systemValue.toLowerCase()}`;
|
|
1452
|
+
return fallback;
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function parseLine(
|
|
1456
|
+
context: ImportContext,
|
|
1457
|
+
line: PptxXmlElement | undefined,
|
|
1458
|
+
partName: string,
|
|
1459
|
+
): PresentationLine {
|
|
1460
|
+
if (!line) return { style: "none", fill: "none", width: 0 };
|
|
1461
|
+
const width = optionalNumber(pptxXmlAttribute(line, "w"));
|
|
1462
|
+
const dash = pptxXmlAttribute(pptxXmlChild(line, "prstDash"), "val");
|
|
1463
|
+
const style =
|
|
1464
|
+
dash === "dot" || dash === "sysDot" ? "dot" : dash && dash !== "solid" ? "dash" : "solid";
|
|
1465
|
+
if (dash && !["solid", "dot", "sysDot", "dash", "sysDash", "lgDash"].includes(dash)) {
|
|
1466
|
+
addUnsupported(context, "theme", partName, "style-approximated");
|
|
1467
|
+
}
|
|
1468
|
+
const fill = parseFill(context, line, "#000000") ?? "#000000";
|
|
1469
|
+
return fill === "none"
|
|
1470
|
+
? { style: "none", fill: "none", width: 0 }
|
|
1471
|
+
: { style, fill, width: width === undefined ? 1 : width / EMU_PER_PIXEL };
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
function parseFramePosition(
|
|
1475
|
+
frame: PptxXmlElement,
|
|
1476
|
+
partName: string,
|
|
1477
|
+
transform: Transform,
|
|
1478
|
+
): PresentationPosition {
|
|
1479
|
+
const xfrm = requiredChild(frame, "xfrm", partName);
|
|
1480
|
+
return applyTransform(rawPositionFromXfrm(xfrm, partName), transform);
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
function rawPositionFromXfrm(xfrm: PptxXmlElement, partName: string): PresentationPosition {
|
|
1484
|
+
const offset = requiredChild(xfrm, "off", partName);
|
|
1485
|
+
const extent = requiredChild(xfrm, "ext", partName);
|
|
1486
|
+
return {
|
|
1487
|
+
left: emuNumber(requiredAttribute(offset, "x", partName), partName),
|
|
1488
|
+
top: emuNumber(requiredAttribute(offset, "y", partName), partName),
|
|
1489
|
+
width: positiveEmu(requiredAttribute(extent, "cx", partName), partName),
|
|
1490
|
+
height: positiveEmu(requiredAttribute(extent, "cy", partName), partName),
|
|
1491
|
+
};
|
|
1492
|
+
}
|
|
1493
|
+
|
|
1494
|
+
function applyTransform(
|
|
1495
|
+
position: PresentationPosition,
|
|
1496
|
+
transform: Transform,
|
|
1497
|
+
): PresentationPosition {
|
|
1498
|
+
return {
|
|
1499
|
+
left: transform.offsetX + position.left * transform.scaleX,
|
|
1500
|
+
top: transform.offsetY + position.top * transform.scaleY,
|
|
1501
|
+
width: position.width * transform.scaleX,
|
|
1502
|
+
height: position.height * transform.scaleY,
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1506
|
+
function rotationFromXfrm(xfrm: PptxXmlElement, partName: string): number {
|
|
1507
|
+
const raw = pptxXmlAttribute(xfrm, "rot");
|
|
1508
|
+
if (raw === undefined) return 0;
|
|
1509
|
+
const value = Number(raw);
|
|
1510
|
+
if (!Number.isSafeInteger(value) || Math.abs(value) > 21_600_000) {
|
|
1511
|
+
throw pptxSecurity("Shape rotation is invalid", "invalid-package", partName);
|
|
1512
|
+
}
|
|
1513
|
+
return value / 60_000;
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function mapGeometry(
|
|
1517
|
+
context: ImportContext,
|
|
1518
|
+
preset: string,
|
|
1519
|
+
partName: string,
|
|
1520
|
+
): PresentationShapeGeometry {
|
|
1521
|
+
const mapped = GEOMETRY_MAP.get(preset);
|
|
1522
|
+
if (mapped) return mapped;
|
|
1523
|
+
addUnsupported(context, "custom-geometry", partName);
|
|
1524
|
+
return "rect";
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1527
|
+
function mapImageGeometry(value: string): "rect" | "roundRect" | "ellipse" {
|
|
1528
|
+
return value === "ellipse" ? "ellipse" : value === "roundRect" ? "roundRect" : "rect";
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function parseChartString(element: PptxXmlElement | undefined): string {
|
|
1532
|
+
if (!element) return "";
|
|
1533
|
+
const points = pptxXmlDescendants(element, "pt")
|
|
1534
|
+
.map((point) => ({
|
|
1535
|
+
index: optionalInteger(pptxXmlAttribute(point, "idx"), "chart") ?? 0,
|
|
1536
|
+
value: pptxXmlText(pptxXmlChild(point, "v")),
|
|
1537
|
+
}))
|
|
1538
|
+
.sort((left, right) => left.index - right.index);
|
|
1539
|
+
if (points.length > 0) return points.map((point) => point.value).join("");
|
|
1540
|
+
return pptxXmlDescendants(element, "t").map(pptxXmlText).join("");
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
function parseChartCategories(element: PptxXmlElement | undefined): string[] {
|
|
1544
|
+
if (!element) return [];
|
|
1545
|
+
return pptxXmlDescendants(element, "pt")
|
|
1546
|
+
.map((point) => ({
|
|
1547
|
+
index: optionalInteger(pptxXmlAttribute(point, "idx"), "chart") ?? 0,
|
|
1548
|
+
value: pptxXmlText(pptxXmlChild(point, "v")),
|
|
1549
|
+
}))
|
|
1550
|
+
.sort((left, right) => left.index - right.index)
|
|
1551
|
+
.map((point) => point.value);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
function parseChartNumbers(
|
|
1555
|
+
element: PptxXmlElement | undefined,
|
|
1556
|
+
context: ImportContext,
|
|
1557
|
+
partName: string,
|
|
1558
|
+
): number[] {
|
|
1559
|
+
if (!element) return [];
|
|
1560
|
+
const points = pptxXmlDescendants(element, "pt")
|
|
1561
|
+
.map((point) => {
|
|
1562
|
+
const index = optionalInteger(pptxXmlAttribute(point, "idx"), partName) ?? 0;
|
|
1563
|
+
const raw = pptxXmlText(pptxXmlChild(point, "v"));
|
|
1564
|
+
const value = Number(raw);
|
|
1565
|
+
if (!Number.isFinite(value)) {
|
|
1566
|
+
throw pptxSecurity("Chart cache contains a non-finite number", "invalid-package", partName);
|
|
1567
|
+
}
|
|
1568
|
+
return { index, value };
|
|
1569
|
+
})
|
|
1570
|
+
.sort((left, right) => left.index - right.index);
|
|
1571
|
+
context.chartPoints += points.length;
|
|
1572
|
+
if (context.chartPoints > context.limits.chartPoints) {
|
|
1573
|
+
throw pptxSecurity("PPTX exceeds its chart point limit", "limit-exceeded", partName);
|
|
1574
|
+
}
|
|
1575
|
+
return points.map((point) => point.value);
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
function parseChartAxis(axis: PptxXmlElement | undefined): PresentationChartConfig["xAxis"] {
|
|
1579
|
+
if (!axis) return undefined;
|
|
1580
|
+
const scaling = pptxXmlChild(axis, "scaling");
|
|
1581
|
+
const minimum = optionalNumber(pptxXmlAttribute(pptxXmlChild(scaling, "min"), "val"));
|
|
1582
|
+
const maximum = optionalNumber(pptxXmlAttribute(pptxXmlChild(scaling, "max"), "val"));
|
|
1583
|
+
return {
|
|
1584
|
+
visible: !booleanAttribute(pptxXmlChild(axis, "delete"), "val"),
|
|
1585
|
+
title: parseChartString(pptxXmlChild(axis, "title")),
|
|
1586
|
+
...(minimum === undefined ? {} : { min: minimum }),
|
|
1587
|
+
...(maximum === undefined ? {} : { max: maximum }),
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
function chartLegendPosition(
|
|
1592
|
+
value: string | undefined,
|
|
1593
|
+
): "left" | "top" | "topRight" | "right" | "bottom" {
|
|
1594
|
+
switch (value) {
|
|
1595
|
+
case "l":
|
|
1596
|
+
return "left";
|
|
1597
|
+
case "t":
|
|
1598
|
+
return "top";
|
|
1599
|
+
case "tr":
|
|
1600
|
+
return "topRight";
|
|
1601
|
+
case "r":
|
|
1602
|
+
return "right";
|
|
1603
|
+
default:
|
|
1604
|
+
return "bottom";
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
|
|
1608
|
+
function parseThemeColors(root: PptxXmlElement, partName: string): ReadonlyMap<string, string> {
|
|
1609
|
+
requireRoot(root, "theme", DRAWING_NAMESPACE, partName);
|
|
1610
|
+
const colors = new Map(DEFAULT_THEME_COLORS);
|
|
1611
|
+
const scheme = pptxXmlDescendants(root, "clrScheme")[0];
|
|
1612
|
+
for (const entry of pptxXmlChildren(scheme)) {
|
|
1613
|
+
const color = pptxXmlChildren(entry)[0];
|
|
1614
|
+
const value = pptxXmlAttribute(color, color?.localName === "sysClr" ? "lastClr" : "val");
|
|
1615
|
+
if (value && /^[0-9A-Fa-f]{6}$/.test(value))
|
|
1616
|
+
colors.set(entry.localName, `#${value.toLowerCase()}`);
|
|
1617
|
+
}
|
|
1618
|
+
return colors;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
function parseSlideSize(root: PptxXmlElement): { width: number; height: number } {
|
|
1622
|
+
const size = requiredChild(root, "sldSz", "ppt/presentation.xml");
|
|
1623
|
+
return {
|
|
1624
|
+
width: positiveEmu(
|
|
1625
|
+
requiredAttribute(size, "cx", "ppt/presentation.xml"),
|
|
1626
|
+
"ppt/presentation.xml",
|
|
1627
|
+
),
|
|
1628
|
+
height: positiveEmu(
|
|
1629
|
+
requiredAttribute(size, "cy", "ppt/presentation.xml"),
|
|
1630
|
+
"ppt/presentation.xml",
|
|
1631
|
+
),
|
|
1632
|
+
};
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
function mapTextAlignment(value: string | undefined): PresentationTextStyle["alignment"] {
|
|
1636
|
+
if (value === "ctr") return "center";
|
|
1637
|
+
if (value === "r") return "right";
|
|
1638
|
+
if (value === "just" || value === "justLow" || value === "dist") return "justify";
|
|
1639
|
+
return value === "l" ? "left" : undefined;
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1642
|
+
function mapVerticalAlignment(
|
|
1643
|
+
value: string | undefined,
|
|
1644
|
+
): PresentationTextStyle["verticalAlignment"] {
|
|
1645
|
+
if (value === "ctr") return "middle";
|
|
1646
|
+
if (value === "b") return "bottom";
|
|
1647
|
+
return value === "t" ? "top" : undefined;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function rasterContentType(
|
|
1651
|
+
partName: string,
|
|
1652
|
+
): "image/png" | "image/jpeg" | "image/gif" | "image/webp" {
|
|
1653
|
+
const extension = partName.slice(partName.lastIndexOf(".") + 1).toLowerCase();
|
|
1654
|
+
if (extension === "png") return "image/png";
|
|
1655
|
+
if (extension === "jpg" || extension === "jpeg") return "image/jpeg";
|
|
1656
|
+
if (extension === "gif") return "image/gif";
|
|
1657
|
+
return "image/webp";
|
|
1658
|
+
}
|
|
1659
|
+
|
|
1660
|
+
function cropPercent(value: string | undefined, partName: string): number {
|
|
1661
|
+
if (value === undefined) return 0;
|
|
1662
|
+
const parsed = Number(value);
|
|
1663
|
+
if (!Number.isFinite(parsed) || parsed < 0 || parsed > 100_000) {
|
|
1664
|
+
throw pptxSecurity("Picture crop percentage is invalid", "invalid-package", partName);
|
|
1665
|
+
}
|
|
1666
|
+
return parsed / 1_000;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
function nonVisualName(element: PptxXmlElement): string | undefined {
|
|
1670
|
+
return pptxXmlAttribute(pptxXmlDescendants(element, "cNvPr")[0], "name");
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
function nonVisualDescription(element: PptxXmlElement): string | undefined {
|
|
1674
|
+
return pptxXmlAttribute(pptxXmlDescendants(element, "cNvPr")[0], "descr");
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
function requiredRelationshipId(
|
|
1678
|
+
element: PptxXmlElement | undefined,
|
|
1679
|
+
partName: string,
|
|
1680
|
+
localName: "id" | "embed" = "id",
|
|
1681
|
+
): string {
|
|
1682
|
+
if (!element)
|
|
1683
|
+
throw pptxSecurity("Relationship reference is missing", "invalid-package", partName);
|
|
1684
|
+
const direct = pptxXmlQualifiedAttribute(element, `r:${localName}`);
|
|
1685
|
+
if (direct) return direct;
|
|
1686
|
+
const candidates = [...element.attributes].filter(
|
|
1687
|
+
([name]) =>
|
|
1688
|
+
name.includes(":") &&
|
|
1689
|
+
name.slice(name.indexOf(":") + 1) === localName &&
|
|
1690
|
+
element.attributeNamespaces?.get(name) === OFFICE_RELATIONSHIP_BASE.slice(0, -1),
|
|
1691
|
+
);
|
|
1692
|
+
if (candidates.length !== 1 || !candidates[0]?.[1]) {
|
|
1693
|
+
throw pptxSecurity(
|
|
1694
|
+
"Relationship reference is missing or ambiguous",
|
|
1695
|
+
"invalid-package",
|
|
1696
|
+
partName,
|
|
1697
|
+
);
|
|
1698
|
+
}
|
|
1699
|
+
return candidates[0][1];
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
function requiredChild(
|
|
1703
|
+
parent: PptxXmlElement | undefined,
|
|
1704
|
+
localName: string,
|
|
1705
|
+
partName: string,
|
|
1706
|
+
): PptxXmlElement {
|
|
1707
|
+
const candidates = pptxXmlChildren(parent).filter((child) => child.localName === localName);
|
|
1708
|
+
if (candidates.length !== 1 || !candidates[0]) {
|
|
1709
|
+
throw pptxSecurity(
|
|
1710
|
+
`Required ${localName} element is missing or duplicated`,
|
|
1711
|
+
"invalid-package",
|
|
1712
|
+
partName,
|
|
1713
|
+
);
|
|
1714
|
+
}
|
|
1715
|
+
return candidates[0];
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
function requireRoot(
|
|
1719
|
+
element: PptxXmlElement,
|
|
1720
|
+
localName: string,
|
|
1721
|
+
namespaceUri: string,
|
|
1722
|
+
partName: string,
|
|
1723
|
+
): void {
|
|
1724
|
+
if (element.localName !== localName || element.namespaceUri !== namespaceUri) {
|
|
1725
|
+
throw pptxSecurity(
|
|
1726
|
+
`OOXML root must be ${localName} in ${namespaceUri}`,
|
|
1727
|
+
"invalid-package",
|
|
1728
|
+
partName,
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
|
|
1733
|
+
function requiredAttribute(
|
|
1734
|
+
element: PptxXmlElement | undefined,
|
|
1735
|
+
localName: string,
|
|
1736
|
+
partName: string,
|
|
1737
|
+
): string {
|
|
1738
|
+
const value = pptxXmlAttribute(element, localName);
|
|
1739
|
+
if (value === undefined || value.length === 0) {
|
|
1740
|
+
throw pptxSecurity(`Required ${localName} attribute is missing`, "invalid-package", partName);
|
|
1741
|
+
}
|
|
1742
|
+
return value;
|
|
1743
|
+
}
|
|
1744
|
+
|
|
1745
|
+
function byRelationshipId(
|
|
1746
|
+
relationships: readonly Relationship[],
|
|
1747
|
+
): ReadonlyMap<string, Relationship> {
|
|
1748
|
+
return new Map(relationships.map((relationship) => [relationship.id, relationship]));
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
function relationshipPartName(sourcePart: string): string {
|
|
1752
|
+
if (sourcePart === "") return "_rels/.rels";
|
|
1753
|
+
const slash = sourcePart.lastIndexOf("/");
|
|
1754
|
+
const directory = slash < 0 ? "" : sourcePart.slice(0, slash + 1);
|
|
1755
|
+
const file = slash < 0 ? sourcePart : sourcePart.slice(slash + 1);
|
|
1756
|
+
return `${directory}_rels/${file}.rels`;
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
function relationshipSourcePart(relationshipPart: string): string {
|
|
1760
|
+
if (relationshipPart === "_rels/.rels") return "";
|
|
1761
|
+
const match = /^(.*\/)?_rels\/([^/]+)\.rels$/.exec(relationshipPart);
|
|
1762
|
+
if (!match) {
|
|
1763
|
+
throw pptxSecurity("Invalid relationships part path", "invalid-package", relationshipPart);
|
|
1764
|
+
}
|
|
1765
|
+
return `${match[1] ?? ""}${match[2] ?? ""}`;
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
function resolvePartTarget(sourcePart: string, target: string, relationshipPart: string): string {
|
|
1769
|
+
if (
|
|
1770
|
+
/[\u0000-\u001f\u007f]/.test(target) ||
|
|
1771
|
+
target.includes("\\") ||
|
|
1772
|
+
target.startsWith("//") ||
|
|
1773
|
+
/%(?:2e|2f|5c)/i.test(target)
|
|
1774
|
+
) {
|
|
1775
|
+
throw pptxSecurity("Unsafe OPC relationship target", "invalid-package", relationshipPart);
|
|
1776
|
+
}
|
|
1777
|
+
const packageAbsolute = target.startsWith("/");
|
|
1778
|
+
const relative = packageAbsolute ? target.slice(1) : target;
|
|
1779
|
+
const base =
|
|
1780
|
+
!packageAbsolute && sourcePart.includes("/")
|
|
1781
|
+
? sourcePart.slice(0, sourcePart.lastIndexOf("/") + 1)
|
|
1782
|
+
: "";
|
|
1783
|
+
const combined = `${base}${relative}`;
|
|
1784
|
+
if (combined.length === 0 || combined.length > 4_096 || combined.includes("//")) {
|
|
1785
|
+
throw pptxSecurity("Invalid OPC relationship target", "invalid-package", relationshipPart);
|
|
1786
|
+
}
|
|
1787
|
+
const output: string[] = [];
|
|
1788
|
+
for (const segment of combined.split("/")) {
|
|
1789
|
+
if (segment === "" || segment === ".") continue;
|
|
1790
|
+
if (segment === "..") {
|
|
1791
|
+
if (output.length === 0) {
|
|
1792
|
+
throw pptxSecurity(
|
|
1793
|
+
"OPC relationship escapes the package root",
|
|
1794
|
+
"invalid-package",
|
|
1795
|
+
relationshipPart,
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
output.pop();
|
|
1799
|
+
} else output.push(segment);
|
|
1800
|
+
if (output.length > 256) {
|
|
1801
|
+
throw pptxSecurity("OPC relationship path is too deep", "limit-exceeded", relationshipPart);
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
return output.join("/");
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
function emuNumber(value: string, partName: string): number {
|
|
1808
|
+
const parsed = Number(value);
|
|
1809
|
+
if (!Number.isSafeInteger(parsed) || Math.abs(parsed) > 9_525_000_000) {
|
|
1810
|
+
throw pptxSecurity(
|
|
1811
|
+
"Presentation geometry is outside supported bounds",
|
|
1812
|
+
"invalid-package",
|
|
1813
|
+
partName,
|
|
1814
|
+
);
|
|
1815
|
+
}
|
|
1816
|
+
return parsed / EMU_PER_PIXEL;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
function positiveEmu(value: string, partName: string): number {
|
|
1820
|
+
const parsed = emuNumber(value, partName);
|
|
1821
|
+
if (parsed <= 0) {
|
|
1822
|
+
throw pptxSecurity("Presentation extent must be positive", "invalid-package", partName);
|
|
1823
|
+
}
|
|
1824
|
+
return parsed;
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
function optionalNumber(value: string | undefined): number | undefined {
|
|
1828
|
+
if (value === undefined) return undefined;
|
|
1829
|
+
const parsed = Number(value);
|
|
1830
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
1831
|
+
}
|
|
1832
|
+
|
|
1833
|
+
function optionalInteger(value: string | undefined, partName: string): number | undefined {
|
|
1834
|
+
if (value === undefined) return undefined;
|
|
1835
|
+
const parsed = Number(value);
|
|
1836
|
+
if (!Number.isSafeInteger(parsed) || parsed < 0) {
|
|
1837
|
+
throw pptxSecurity("OOXML integer is invalid", "invalid-package", partName);
|
|
1838
|
+
}
|
|
1839
|
+
return parsed;
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
function optionalPositiveInteger(value: string | undefined, partName: string): number | undefined {
|
|
1843
|
+
const parsed = optionalInteger(value, partName);
|
|
1844
|
+
if (parsed === undefined) return undefined;
|
|
1845
|
+
if (parsed <= 0 || parsed > 1_024) {
|
|
1846
|
+
throw pptxSecurity("OOXML span is outside supported bounds", "invalid-package", partName);
|
|
1847
|
+
}
|
|
1848
|
+
return parsed;
|
|
1849
|
+
}
|
|
1850
|
+
|
|
1851
|
+
function booleanAttribute(element: PptxXmlElement | undefined, name: string): boolean {
|
|
1852
|
+
const value = pptxXmlAttribute(element, name);
|
|
1853
|
+
return value === "1" || value === "true" || value === "on";
|
|
1854
|
+
}
|
|
1855
|
+
|
|
1856
|
+
function consumeElement(context: ImportContext, partName: string): void {
|
|
1857
|
+
context.elements += 1;
|
|
1858
|
+
if (context.elements > context.limits.elements) {
|
|
1859
|
+
throw pptxSecurity("PPTX exceeds its element limit", "limit-exceeded", partName);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
function consumeText(context: ImportContext, characters: number, partName: string): void {
|
|
1864
|
+
context.textCharacters += characters;
|
|
1865
|
+
if (context.textCharacters > context.limits.textCharacters) {
|
|
1866
|
+
throw pptxSecurity("PPTX exceeds its text character limit", "limit-exceeded", partName);
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
function addUnsupported(
|
|
1871
|
+
context: ImportContext,
|
|
1872
|
+
feature: PresentationFidelityIssue["feature"],
|
|
1873
|
+
partName: string,
|
|
1874
|
+
code: PresentationFidelityIssue["code"] = "content-preserved-in-source",
|
|
1875
|
+
): void {
|
|
1876
|
+
context.unsupportedParts.add(partName);
|
|
1877
|
+
context.issues.push({
|
|
1878
|
+
code,
|
|
1879
|
+
severity: code === "content-will-be-discarded" ? "error" : "warning",
|
|
1880
|
+
feature,
|
|
1881
|
+
message:
|
|
1882
|
+
code === "style-approximated"
|
|
1883
|
+
? `${feature} styling is represented by the closest editable model style`
|
|
1884
|
+
: `${feature} content is retained in the validated source package but is not editable`,
|
|
1885
|
+
parts: [partName],
|
|
1886
|
+
});
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
function fidelityIssue(
|
|
1890
|
+
feature: PresentationFidelityIssue["feature"],
|
|
1891
|
+
parts: readonly string[],
|
|
1892
|
+
): PresentationFidelityIssue {
|
|
1893
|
+
return {
|
|
1894
|
+
code: "content-preserved-in-source",
|
|
1895
|
+
severity: "warning",
|
|
1896
|
+
feature,
|
|
1897
|
+
message: `${feature} content is retained in the validated source package but is not editable`,
|
|
1898
|
+
parts,
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
function deduplicateIssues(
|
|
1903
|
+
issues: readonly PresentationFidelityIssue[],
|
|
1904
|
+
): PresentationFidelityIssue[] {
|
|
1905
|
+
const result = new Map<string, PresentationFidelityIssue>();
|
|
1906
|
+
for (const issue of issues) {
|
|
1907
|
+
const key = `${issue.code}\0${issue.feature}\0${(issue.parts ?? []).join("\0")}`;
|
|
1908
|
+
result.set(key, issue);
|
|
1909
|
+
}
|
|
1910
|
+
return [...result.values()];
|
|
1911
|
+
}
|
|
1912
|
+
|
|
1913
|
+
function pptxSecurity(
|
|
1914
|
+
message: string,
|
|
1915
|
+
code: PresentationSecurityError["code"],
|
|
1916
|
+
entryName?: string,
|
|
1917
|
+
): PresentationSecurityError {
|
|
1918
|
+
return new PresentationSecurityError(
|
|
1919
|
+
entryName ? `${message}: ${entryName}` : message,
|
|
1920
|
+
code,
|
|
1921
|
+
entryName,
|
|
1922
|
+
);
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
function pptxZipFailure(
|
|
1926
|
+
kind: Parameters<BoundedZipFailure>[0],
|
|
1927
|
+
message: string,
|
|
1928
|
+
entryName?: string,
|
|
1929
|
+
): never {
|
|
1930
|
+
throw pptxSecurity(
|
|
1931
|
+
message,
|
|
1932
|
+
kind === "limit"
|
|
1933
|
+
? "limit-exceeded"
|
|
1934
|
+
: kind === "encrypted"
|
|
1935
|
+
? "encrypted-content"
|
|
1936
|
+
: "invalid-package",
|
|
1937
|
+
entryName,
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
const IDENTITY_TRANSFORM: Transform = { offsetX: 0, offsetY: 0, scaleX: 1, scaleY: 1 };
|
|
1942
|
+
|
|
1943
|
+
const GEOMETRY_MAP = new Map<string, PresentationShapeGeometry>([
|
|
1944
|
+
["rect", "rect"],
|
|
1945
|
+
["roundRect", "roundRect"],
|
|
1946
|
+
["ellipse", "ellipse"],
|
|
1947
|
+
["triangle", "triangle"],
|
|
1948
|
+
["rtTriangle", "triangle"],
|
|
1949
|
+
["rightArrow", "rightArrow"],
|
|
1950
|
+
["line", "line"],
|
|
1951
|
+
]);
|
|
1952
|
+
|
|
1953
|
+
const CHART_ELEMENT_TYPES = new Map<string, PresentationChartType>([
|
|
1954
|
+
["barChart", "bar"],
|
|
1955
|
+
["bar3DChart", "bar"],
|
|
1956
|
+
["lineChart", "line"],
|
|
1957
|
+
["line3DChart", "line"],
|
|
1958
|
+
["areaChart", "area"],
|
|
1959
|
+
["area3DChart", "area"],
|
|
1960
|
+
["pieChart", "pie"],
|
|
1961
|
+
["pie3DChart", "pie"],
|
|
1962
|
+
["doughnutChart", "doughnut"],
|
|
1963
|
+
["scatterChart", "scatter"],
|
|
1964
|
+
["bubbleChart", "bubble"],
|
|
1965
|
+
["radarChart", "radar"],
|
|
1966
|
+
]);
|
|
1967
|
+
|
|
1968
|
+
const DEFAULT_THEME_COLORS = new Map<string, string>([
|
|
1969
|
+
["dk1", "#000000"],
|
|
1970
|
+
["lt1", "#ffffff"],
|
|
1971
|
+
["dk2", "#1f497d"],
|
|
1972
|
+
["lt2", "#eeeceed"],
|
|
1973
|
+
["accent1", "#4472c4"],
|
|
1974
|
+
["accent2", "#ed7d31"],
|
|
1975
|
+
["accent3", "#a5a5a5"],
|
|
1976
|
+
["accent4", "#ffc000"],
|
|
1977
|
+
["accent5", "#5b9bd5"],
|
|
1978
|
+
["accent6", "#70ad47"],
|
|
1979
|
+
["hlink", "#0563c1"],
|
|
1980
|
+
["folHlink", "#954f72"],
|
|
1981
|
+
["tx1", "#000000"],
|
|
1982
|
+
["bg1", "#ffffff"],
|
|
1983
|
+
["tx2", "#1f497d"],
|
|
1984
|
+
["bg2", "#eeeceed"],
|
|
1985
|
+
]);
|
|
1986
|
+
|
|
1987
|
+
const KNOWN_RELATIONSHIP_SUFFIXES = new Set([
|
|
1988
|
+
"officeDocument",
|
|
1989
|
+
"core-properties",
|
|
1990
|
+
"extended-properties",
|
|
1991
|
+
"custom-properties",
|
|
1992
|
+
"slideMaster",
|
|
1993
|
+
"slideLayout",
|
|
1994
|
+
"slide",
|
|
1995
|
+
"notesMaster",
|
|
1996
|
+
"notesSlide",
|
|
1997
|
+
"theme",
|
|
1998
|
+
"presProps",
|
|
1999
|
+
"viewProps",
|
|
2000
|
+
"tableStyles",
|
|
2001
|
+
"chart",
|
|
2002
|
+
"image",
|
|
2003
|
+
"package",
|
|
2004
|
+
"hyperlink",
|
|
2005
|
+
"handoutMaster",
|
|
2006
|
+
"font",
|
|
2007
|
+
"tags",
|
|
2008
|
+
"customXml",
|
|
2009
|
+
"comments",
|
|
2010
|
+
"commentAuthors",
|
|
2011
|
+
"diagramData",
|
|
2012
|
+
"diagramLayout",
|
|
2013
|
+
"diagramColors",
|
|
2014
|
+
"diagramQuickStyle",
|
|
2015
|
+
]);
|
|
2016
|
+
|
|
2017
|
+
const ACTIVE_RELATIONSHIP_SUFFIX =
|
|
2018
|
+
/^(?:vbaProject|activeX|oleObject|audio|video|media|externalLink|attachedTemplate|control|webVideo|linkedData|externalData)$/i;
|
|
2019
|
+
|
|
2020
|
+
const SOURCE_ONLY_RELATIONSHIP_SUFFIXES = new Set([
|
|
2021
|
+
"handoutMaster",
|
|
2022
|
+
"font",
|
|
2023
|
+
"tags",
|
|
2024
|
+
"customXml",
|
|
2025
|
+
"comments",
|
|
2026
|
+
"commentAuthors",
|
|
2027
|
+
"diagramData",
|
|
2028
|
+
"diagramLayout",
|
|
2029
|
+
"diagramColors",
|
|
2030
|
+
"diagramQuickStyle",
|
|
2031
|
+
]);
|
|
2032
|
+
|
|
2033
|
+
const ALLOWED_EXACT_RELATIONSHIPS = new Set([
|
|
2034
|
+
"http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties",
|
|
2035
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties",
|
|
2036
|
+
"http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties",
|
|
2037
|
+
]);
|
|
2038
|
+
|
|
2039
|
+
const KNOWN_DEFAULT_CONTENT_TYPES = new Set([
|
|
2040
|
+
"rels\0application/vnd.openxmlformats-package.relationships+xml",
|
|
2041
|
+
"xml\0application/xml",
|
|
2042
|
+
"png\0image/png",
|
|
2043
|
+
"jpg\0image/jpeg",
|
|
2044
|
+
"jpeg\0image/jpeg",
|
|
2045
|
+
"jpg\0image/jpg",
|
|
2046
|
+
"gif\0image/gif",
|
|
2047
|
+
"webp\0image/webp",
|
|
2048
|
+
"svg\0image/svg+xml",
|
|
2049
|
+
"m4v\0video/mp4",
|
|
2050
|
+
"mp4\0video/mp4",
|
|
2051
|
+
"vml\0application/vnd.openxmlformats-officedocument.vmlDrawing",
|
|
2052
|
+
"xlsx\0application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
2053
|
+
]);
|
|
2054
|
+
|
|
2055
|
+
const KNOWN_PPTX_PART_PATTERNS: readonly RegExp[] = [
|
|
2056
|
+
/^\[Content_Types\]\.xml$/,
|
|
2057
|
+
/^_rels\/\.rels$/,
|
|
2058
|
+
/^docProps\/(?:app|core|custom)\.xml$/,
|
|
2059
|
+
/^ppt\/presentation\.xml$/,
|
|
2060
|
+
/^ppt\/(?:presProps|viewProps|tableStyles)\.xml$/,
|
|
2061
|
+
/^ppt\/(?:_rels\/presentation\.xml|slides\/_rels\/slide\d+\.xml|slideMasters\/_rels\/slideMaster\d+\.xml|slideLayouts\/_rels\/slideLayout\d+\.xml|notesSlides\/_rels\/notesSlide\d+\.xml|notesMasters\/_rels\/notesMaster\d+\.xml|charts\/_rels\/chart\d+\.xml)\.rels$/,
|
|
2062
|
+
/^ppt\/(?:slides\/slide\d+|slideMasters\/slideMaster\d+|slideLayouts\/slideLayout\d+|notesSlides\/notesSlide\d+|notesMasters\/notesMaster\d+|charts\/chart\d+|theme\/theme\d+)\.xml$/,
|
|
2063
|
+
/^ppt\/media\/[^/]+\.(?:png|jpe?g|gif|webp)$/i,
|
|
2064
|
+
/^ppt\/embeddings\/[^/]+\.xlsx$/i,
|
|
2065
|
+
];
|