@files-preview-app/preview-file 1.1.5 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/angular.cjs +1630 -54
- package/dist/angular.cjs.map +1 -1
- package/dist/angular.js +1629 -53
- package/dist/angular.js.map +1 -1
- package/dist/index.cjs +1637 -55
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +12 -3
- package/dist/index.d.ts +12 -3
- package/dist/index.js +1631 -55
- package/dist/index.js.map +1 -1
- package/dist/react.cjs +1630 -54
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +5 -0
- package/dist/react.d.ts +5 -0
- package/dist/react.js +1629 -53
- package/dist/react.js.map +1 -1
- package/dist/vue.cjs +1630 -54
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +1629 -53
- package/dist/vue.js.map +1 -1
- package/package.json +10 -3
package/dist/index.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
import
|
|
1
|
+
import DOMPurify5 from 'dompurify';
|
|
2
2
|
import * as docx from 'docx-preview';
|
|
3
|
-
import
|
|
3
|
+
import * as XLSX from 'xlsx';
|
|
4
4
|
import hljs from 'highlight.js';
|
|
5
|
-
import { unzip } from 'fflate';
|
|
5
|
+
import { unzip, unzipSync, strFromU8 } from 'fflate';
|
|
6
6
|
import { marked } from 'marked';
|
|
7
7
|
import { PptxRenderer } from 'pptx-browser';
|
|
8
8
|
import * as THREE from 'three';
|
|
9
9
|
import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
|
|
10
10
|
import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
|
|
11
11
|
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
|
|
12
|
+
import { RTFJS } from 'rtf.js';
|
|
12
13
|
|
|
13
14
|
// ../core/dist/index.js
|
|
14
15
|
var EventEmitter = class {
|
|
@@ -72,7 +73,9 @@ var MAGIC_NUMBERS = [
|
|
|
72
73
|
{ bytes: [37, 80, 68, 70], mime: "application/pdf" },
|
|
73
74
|
// %PDF
|
|
74
75
|
{ bytes: [80, 75, 3, 4], mime: "application/zip" },
|
|
75
|
-
// PK.. (ZIP/DOCX/XLSX/PPTX)
|
|
76
|
+
// PK.. (ZIP/DOCX/XLSX/PPTX/ODT/ODS/ODP)
|
|
77
|
+
{ bytes: [208, 207, 17, 224, 161, 177, 26, 225], mime: "application/x-cfbf" },
|
|
78
|
+
// CFBF/OLE2 (DOC/XLS/PPT)
|
|
76
79
|
{ bytes: [137, 80, 78, 71, 13, 10, 26, 10], mime: "image/png" },
|
|
77
80
|
// PNG
|
|
78
81
|
{ bytes: [255, 216, 255], mime: "image/jpeg" },
|
|
@@ -107,14 +110,40 @@ var MAGIC_NUMBERS = [
|
|
|
107
110
|
// RTF
|
|
108
111
|
];
|
|
109
112
|
var EXTENSION_MIME_MAP = {
|
|
110
|
-
// Documents
|
|
113
|
+
// Documents — Modern OOXML
|
|
111
114
|
".pdf": "application/pdf",
|
|
112
115
|
".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
116
|
+
".docm": "application/vnd.ms-word.document.macroEnabled.12",
|
|
117
|
+
".dotx": "application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
|
118
|
+
".dotm": "application/vnd.ms-word.template.macroEnabled.12",
|
|
113
119
|
".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
120
|
+
".xlsm": "application/vnd.ms-excel.sheet.macroEnabled.12",
|
|
121
|
+
".xlsb": "application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
|
122
|
+
".xltx": "application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
|
123
|
+
".xltm": "application/vnd.ms-excel.template.macroEnabled.12",
|
|
114
124
|
".xls": "application/vnd.ms-excel",
|
|
115
125
|
".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
126
|
+
".ppsx": "application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
|
127
|
+
".pptm": "application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
|
128
|
+
".ppsm": "application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
|
|
129
|
+
".potx": "application/vnd.openxmlformats-officedocument.presentationml.template",
|
|
130
|
+
".potm": "application/vnd.ms-powerpoint.template.macroEnabled.12",
|
|
116
131
|
".csv": "text/csv",
|
|
117
132
|
".tsv": "text/tab-separated-values",
|
|
133
|
+
// Documents — Legacy Binary (CFBF/OLE2)
|
|
134
|
+
".doc": "application/msword",
|
|
135
|
+
".dot": "application/msword",
|
|
136
|
+
".ppt": "application/vnd.ms-powerpoint",
|
|
137
|
+
".pps": "application/vnd.ms-powerpoint",
|
|
138
|
+
".pot": "application/vnd.ms-powerpoint",
|
|
139
|
+
// Documents — OpenDocument (ODF)
|
|
140
|
+
".odt": "application/vnd.oasis.opendocument.text",
|
|
141
|
+
".ods": "application/vnd.oasis.opendocument.spreadsheet",
|
|
142
|
+
".odp": "application/vnd.oasis.opendocument.presentation",
|
|
143
|
+
".odg": "application/vnd.oasis.opendocument.graphics",
|
|
144
|
+
".odf": "application/vnd.oasis.opendocument.formula",
|
|
145
|
+
// Documents — RTF
|
|
146
|
+
".rtf": "text/rtf",
|
|
118
147
|
// Images
|
|
119
148
|
".png": "image/png",
|
|
120
149
|
".jpg": "image/jpeg",
|
|
@@ -208,7 +237,7 @@ function detectMagicBytes(buffer) {
|
|
|
208
237
|
}
|
|
209
238
|
function detectOoxmlType(buffer) {
|
|
210
239
|
const text = new TextDecoder("ascii", { fatal: false }).decode(
|
|
211
|
-
new Uint8Array(buffer.slice(0,
|
|
240
|
+
new Uint8Array(buffer.slice(0, 8e3))
|
|
212
241
|
);
|
|
213
242
|
if (text.includes("word/")) {
|
|
214
243
|
return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
@@ -219,6 +248,21 @@ function detectOoxmlType(buffer) {
|
|
|
219
248
|
if (text.includes("ppt/")) {
|
|
220
249
|
return "application/vnd.openxmlformats-officedocument.presentationml.presentation";
|
|
221
250
|
}
|
|
251
|
+
if (text.includes("application/vnd.oasis.opendocument.text")) {
|
|
252
|
+
return "application/vnd.oasis.opendocument.text";
|
|
253
|
+
}
|
|
254
|
+
if (text.includes("application/vnd.oasis.opendocument.spreadsheet")) {
|
|
255
|
+
return "application/vnd.oasis.opendocument.spreadsheet";
|
|
256
|
+
}
|
|
257
|
+
if (text.includes("application/vnd.oasis.opendocument.presentation")) {
|
|
258
|
+
return "application/vnd.oasis.opendocument.presentation";
|
|
259
|
+
}
|
|
260
|
+
if (text.includes("application/vnd.oasis.opendocument.graphics")) {
|
|
261
|
+
return "application/vnd.oasis.opendocument.graphics";
|
|
262
|
+
}
|
|
263
|
+
if (text.includes("application/vnd.oasis.opendocument.formula")) {
|
|
264
|
+
return "application/vnd.oasis.opendocument.formula";
|
|
265
|
+
}
|
|
222
266
|
return "application/zip";
|
|
223
267
|
}
|
|
224
268
|
function extractExtension(nameOrUrl) {
|
|
@@ -295,12 +339,12 @@ async function sourceToArrayBuffer(source, signal) {
|
|
|
295
339
|
return { buffer, metadata };
|
|
296
340
|
}
|
|
297
341
|
function sanitizeHTML(html) {
|
|
298
|
-
return
|
|
342
|
+
return DOMPurify5.sanitize(html, {
|
|
299
343
|
USE_PROFILES: { html: true }
|
|
300
344
|
});
|
|
301
345
|
}
|
|
302
346
|
function sanitizeSVG(svg) {
|
|
303
|
-
return
|
|
347
|
+
return DOMPurify5.sanitize(svg, {
|
|
304
348
|
USE_PROFILES: { svg: true, svgFilters: true }
|
|
305
349
|
});
|
|
306
350
|
}
|
|
@@ -907,6 +951,192 @@ var FilePreviewViewer = class {
|
|
|
907
951
|
this.contentEl.appendChild(errorEl);
|
|
908
952
|
}
|
|
909
953
|
};
|
|
954
|
+
var CfbfReader = class {
|
|
955
|
+
view;
|
|
956
|
+
u8;
|
|
957
|
+
sectorSize;
|
|
958
|
+
miniSectorSize;
|
|
959
|
+
miniStreamCutoff;
|
|
960
|
+
fat = [];
|
|
961
|
+
miniFat = [];
|
|
962
|
+
entries = /* @__PURE__ */ new Map();
|
|
963
|
+
miniStream = new Uint8Array(0);
|
|
964
|
+
constructor(buffer) {
|
|
965
|
+
this.u8 = new Uint8Array(buffer);
|
|
966
|
+
this.view = new DataView(buffer);
|
|
967
|
+
if (!this.isValid()) {
|
|
968
|
+
throw new Error("Invalid CFBF / OLE2 file header signature");
|
|
969
|
+
}
|
|
970
|
+
const sectorShift = this.view.getUint16(30, true);
|
|
971
|
+
this.sectorSize = 1 << sectorShift;
|
|
972
|
+
const miniSectorShift = this.view.getUint16(32, true);
|
|
973
|
+
this.miniSectorSize = 1 << miniSectorShift;
|
|
974
|
+
this.miniStreamCutoff = this.view.getUint32(56, true) || 4096;
|
|
975
|
+
this.parseFat();
|
|
976
|
+
this.parseDirectory();
|
|
977
|
+
this.parseMiniFat();
|
|
978
|
+
}
|
|
979
|
+
/** Check if the magic 8-byte signature matches [MS-CFB] */
|
|
980
|
+
isValid() {
|
|
981
|
+
if (this.u8.length < 512) return false;
|
|
982
|
+
const sig = [208, 207, 17, 224, 161, 177, 26, 225];
|
|
983
|
+
return sig.every((b, i) => this.u8[i] === b);
|
|
984
|
+
}
|
|
985
|
+
/** Return all directory stream/storage entries found in the file */
|
|
986
|
+
listEntries() {
|
|
987
|
+
return Array.from(this.entries.values());
|
|
988
|
+
}
|
|
989
|
+
/** Check if a stream exists */
|
|
990
|
+
hasStream(name) {
|
|
991
|
+
return this.entries.has(name) || this.findEntryCaseInsensitive(name) !== null;
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Read raw bytes of a named stream (e.g. "WordDocument", "PowerPoint Document", "Workbook")
|
|
995
|
+
*/
|
|
996
|
+
readStream(name) {
|
|
997
|
+
const entry = this.entries.get(name) || this.findEntryCaseInsensitive(name);
|
|
998
|
+
if (!entry) return null;
|
|
999
|
+
if (entry.size === 0) return new Uint8Array(0);
|
|
1000
|
+
if (entry.size < this.miniStreamCutoff && entry.type !== 5) {
|
|
1001
|
+
return this.readMiniStream(entry.startSector, entry.size);
|
|
1002
|
+
} else {
|
|
1003
|
+
return this.readRegularStream(entry.startSector, entry.size);
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
findEntryCaseInsensitive(name) {
|
|
1007
|
+
const lower = name.toLowerCase();
|
|
1008
|
+
for (const [key, val] of this.entries.entries()) {
|
|
1009
|
+
if (key.toLowerCase() === lower) return val;
|
|
1010
|
+
}
|
|
1011
|
+
return null;
|
|
1012
|
+
}
|
|
1013
|
+
getSectorOffset(sectId) {
|
|
1014
|
+
return (sectId + 1) * this.sectorSize;
|
|
1015
|
+
}
|
|
1016
|
+
parseFat() {
|
|
1017
|
+
this.view.getUint32(44, true);
|
|
1018
|
+
const difat = [];
|
|
1019
|
+
for (let i = 0; i < 109; i++) {
|
|
1020
|
+
const sect = this.view.getUint32(76 + i * 4, true);
|
|
1021
|
+
if (sect !== 4294967295 && sect !== 4294967294) {
|
|
1022
|
+
difat.push(sect);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
let difatSect = this.view.getUint32(68, true);
|
|
1026
|
+
const csectDif = this.view.getUint32(72, true);
|
|
1027
|
+
const entriesPerSector = this.sectorSize / 4 - 1;
|
|
1028
|
+
for (let i = 0; i < csectDif && difatSect < 4294967294; i++) {
|
|
1029
|
+
const offset = this.getSectorOffset(difatSect);
|
|
1030
|
+
for (let j = 0; j < entriesPerSector; j++) {
|
|
1031
|
+
const sect = this.view.getUint32(offset + j * 4, true);
|
|
1032
|
+
if (sect !== 4294967295 && sect !== 4294967294) {
|
|
1033
|
+
difat.push(sect);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
difatSect = this.view.getUint32(offset + entriesPerSector * 4, true);
|
|
1037
|
+
}
|
|
1038
|
+
for (const fatSect of difat) {
|
|
1039
|
+
if (fatSect >= 4294967294) continue;
|
|
1040
|
+
const offset = this.getSectorOffset(fatSect);
|
|
1041
|
+
const count = this.sectorSize / 4;
|
|
1042
|
+
for (let j = 0; j < count; j++) {
|
|
1043
|
+
this.fat.push(this.view.getUint32(offset + j * 4, true));
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
parseDirectory() {
|
|
1048
|
+
const dirStartSect = this.view.getUint32(48, true);
|
|
1049
|
+
const dirBytes = this.readRegularStream(dirStartSect);
|
|
1050
|
+
const dirView = new DataView(dirBytes.buffer, dirBytes.byteOffset, dirBytes.byteLength);
|
|
1051
|
+
const entryCount = dirBytes.length / 128;
|
|
1052
|
+
for (let i = 0; i < entryCount; i++) {
|
|
1053
|
+
const offset = i * 128;
|
|
1054
|
+
const type = dirView.getUint8(offset + 66);
|
|
1055
|
+
if (type === 0) continue;
|
|
1056
|
+
const nameLen = dirView.getUint16(offset + 64, true);
|
|
1057
|
+
let name = "";
|
|
1058
|
+
if (nameLen > 2) {
|
|
1059
|
+
const charCount = nameLen / 2 - 1;
|
|
1060
|
+
const chars = [];
|
|
1061
|
+
for (let c = 0; c < charCount; c++) {
|
|
1062
|
+
const charCode = dirView.getUint16(offset + c * 2, true);
|
|
1063
|
+
chars.push(String.fromCharCode(charCode));
|
|
1064
|
+
}
|
|
1065
|
+
name = chars.join("");
|
|
1066
|
+
}
|
|
1067
|
+
const startSector = dirView.getUint32(offset + 116, true);
|
|
1068
|
+
const size = dirView.getUint32(offset + 120, true);
|
|
1069
|
+
const entry = { name, type, size, startSector };
|
|
1070
|
+
this.entries.set(name, entry);
|
|
1071
|
+
if (type === 5) {
|
|
1072
|
+
this.miniStream = this.readRegularStream(startSector, size);
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
parseMiniFat() {
|
|
1077
|
+
const miniFatStart = this.view.getUint32(60, true);
|
|
1078
|
+
const csectMiniFat = this.view.getUint32(64, true);
|
|
1079
|
+
if (miniFatStart >= 4294967294 || csectMiniFat === 0) return;
|
|
1080
|
+
let sect = miniFatStart;
|
|
1081
|
+
for (let i = 0; i < csectMiniFat && sect < 4294967294; i++) {
|
|
1082
|
+
const offset = this.getSectorOffset(sect);
|
|
1083
|
+
const count = this.sectorSize / 4;
|
|
1084
|
+
for (let j = 0; j < count; j++) {
|
|
1085
|
+
this.miniFat.push(this.view.getUint32(offset + j * 4, true));
|
|
1086
|
+
}
|
|
1087
|
+
sect = this.fat[sect] ?? 4294967294;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
readRegularStream(startSector, targetSize) {
|
|
1091
|
+
if (startSector >= 4294967294) return new Uint8Array(0);
|
|
1092
|
+
const chunks = [];
|
|
1093
|
+
let curr = startSector;
|
|
1094
|
+
let totalBytes = 0;
|
|
1095
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1096
|
+
while (curr < 4294967294 && !visited.has(curr)) {
|
|
1097
|
+
visited.add(curr);
|
|
1098
|
+
const offset = this.getSectorOffset(curr);
|
|
1099
|
+
if (offset + this.sectorSize <= this.u8.length) {
|
|
1100
|
+
chunks.push(this.u8.subarray(offset, offset + this.sectorSize));
|
|
1101
|
+
totalBytes += this.sectorSize;
|
|
1102
|
+
}
|
|
1103
|
+
curr = this.fat[curr] ?? 4294967294;
|
|
1104
|
+
}
|
|
1105
|
+
const result = new Uint8Array(totalBytes);
|
|
1106
|
+
let pos = 0;
|
|
1107
|
+
for (const chunk of chunks) {
|
|
1108
|
+
result.set(chunk, pos);
|
|
1109
|
+
pos += chunk.length;
|
|
1110
|
+
}
|
|
1111
|
+
if (targetSize !== void 0 && targetSize < result.length) {
|
|
1112
|
+
return result.subarray(0, targetSize);
|
|
1113
|
+
}
|
|
1114
|
+
return result;
|
|
1115
|
+
}
|
|
1116
|
+
readMiniStream(startSector, targetSize) {
|
|
1117
|
+
if (this.miniStream.length === 0 || startSector >= 4294967294) return new Uint8Array(0);
|
|
1118
|
+
const chunks = [];
|
|
1119
|
+
let curr = startSector;
|
|
1120
|
+
let totalBytes = 0;
|
|
1121
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1122
|
+
while (curr < 4294967294 && !visited.has(curr)) {
|
|
1123
|
+
visited.add(curr);
|
|
1124
|
+
const offset = curr * this.miniSectorSize;
|
|
1125
|
+
if (offset + this.miniSectorSize <= this.miniStream.length) {
|
|
1126
|
+
chunks.push(this.miniStream.subarray(offset, offset + this.miniSectorSize));
|
|
1127
|
+
totalBytes += this.miniSectorSize;
|
|
1128
|
+
}
|
|
1129
|
+
curr = this.miniFat[curr] ?? 4294967294;
|
|
1130
|
+
}
|
|
1131
|
+
const result = new Uint8Array(totalBytes);
|
|
1132
|
+
let pos = 0;
|
|
1133
|
+
for (const chunk of chunks) {
|
|
1134
|
+
result.set(chunk, pos);
|
|
1135
|
+
pos += chunk.length;
|
|
1136
|
+
}
|
|
1137
|
+
return result.subarray(0, targetSize);
|
|
1138
|
+
}
|
|
1139
|
+
};
|
|
910
1140
|
|
|
911
1141
|
// ../plugins/pdf/dist/index.js
|
|
912
1142
|
var PdfPlugin = class {
|
|
@@ -1317,13 +1547,18 @@ function mediaPlugin() {
|
|
|
1317
1547
|
var DocxPlugin = class {
|
|
1318
1548
|
id = "docx";
|
|
1319
1549
|
name = "Word Document Preview";
|
|
1320
|
-
extensions = [".docx"];
|
|
1321
|
-
mimeTypes = [
|
|
1550
|
+
extensions = [".docx", ".docm", ".dotx", ".dotm"];
|
|
1551
|
+
mimeTypes = [
|
|
1552
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
1553
|
+
"application/vnd.ms-word.document.macroEnabled.12",
|
|
1554
|
+
"application/vnd.openxmlformats-officedocument.wordprocessingml.template",
|
|
1555
|
+
"application/vnd.ms-word.template.macroEnabled.12"
|
|
1556
|
+
];
|
|
1322
1557
|
weight = 80;
|
|
1323
1558
|
supports(file) {
|
|
1324
1559
|
const ext = file.metadata.extension?.toLowerCase();
|
|
1325
1560
|
const mime = file.metadata.mimeType?.toLowerCase();
|
|
1326
|
-
return ext
|
|
1561
|
+
return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
|
|
1327
1562
|
}
|
|
1328
1563
|
getToolbarActions(instance) {
|
|
1329
1564
|
return [
|
|
@@ -1447,11 +1682,16 @@ function docxPlugin() {
|
|
|
1447
1682
|
}
|
|
1448
1683
|
var ExcelPlugin = class {
|
|
1449
1684
|
id = "excel";
|
|
1450
|
-
name = "Excel
|
|
1451
|
-
extensions = [".xlsx", ".xls"];
|
|
1685
|
+
name = "Spreadsheet Preview (Excel / OpenDocument)";
|
|
1686
|
+
extensions = [".xlsx", ".xls", ".xlsm", ".xlsb", ".xltx", ".xltm", ".ods"];
|
|
1452
1687
|
mimeTypes = [
|
|
1453
1688
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
1454
|
-
"application/vnd.ms-excel"
|
|
1689
|
+
"application/vnd.ms-excel",
|
|
1690
|
+
"application/vnd.ms-excel.sheet.macroEnabled.12",
|
|
1691
|
+
"application/vnd.ms-excel.sheet.binary.macroEnabled.12",
|
|
1692
|
+
"application/vnd.openxmlformats-officedocument.spreadsheetml.template",
|
|
1693
|
+
"application/vnd.ms-excel.template.macroEnabled.12",
|
|
1694
|
+
"application/vnd.oasis.opendocument.spreadsheet"
|
|
1455
1695
|
];
|
|
1456
1696
|
weight = 80;
|
|
1457
1697
|
supports(file) {
|
|
@@ -1515,70 +1755,111 @@ var ExcelPlugin = class {
|
|
|
1515
1755
|
}
|
|
1516
1756
|
async render(ctx) {
|
|
1517
1757
|
const container = document.createElement("div");
|
|
1758
|
+
container.className = "fp-excel-container";
|
|
1518
1759
|
container.style.display = "flex";
|
|
1519
1760
|
container.style.flexDirection = "column";
|
|
1520
1761
|
container.style.width = "100%";
|
|
1521
1762
|
container.style.height = "100%";
|
|
1522
1763
|
container.style.overflow = "hidden";
|
|
1523
1764
|
const contentArea = document.createElement("div");
|
|
1765
|
+
contentArea.className = "fp-excel-content";
|
|
1524
1766
|
contentArea.style.flex = "1";
|
|
1525
1767
|
contentArea.style.overflow = "auto";
|
|
1526
1768
|
contentArea.style.padding = "16px";
|
|
1527
1769
|
contentArea.style.transformOrigin = "top left";
|
|
1528
1770
|
const tabsArea = document.createElement("div");
|
|
1771
|
+
tabsArea.className = "fp-excel-tabs";
|
|
1529
1772
|
tabsArea.style.display = "flex";
|
|
1530
|
-
tabsArea.style.gap = "
|
|
1773
|
+
tabsArea.style.gap = "6px";
|
|
1531
1774
|
tabsArea.style.padding = "8px 16px";
|
|
1532
1775
|
tabsArea.style.borderTop = "1px solid #e0e0e0";
|
|
1533
1776
|
tabsArea.style.backgroundColor = "#fafafa";
|
|
1534
1777
|
tabsArea.style.overflowX = "auto";
|
|
1778
|
+
tabsArea.style.flexShrink = "0";
|
|
1535
1779
|
container.appendChild(contentArea);
|
|
1536
1780
|
container.appendChild(tabsArea);
|
|
1537
1781
|
ctx.container.appendChild(container);
|
|
1538
1782
|
let scale = 1;
|
|
1539
1783
|
let currentSheetIndex = 1;
|
|
1540
|
-
|
|
1784
|
+
let sheetNames = [];
|
|
1785
|
+
let wb = null;
|
|
1541
1786
|
try {
|
|
1542
|
-
|
|
1543
|
-
|
|
1787
|
+
wb = XLSX.read(new Uint8Array(ctx.buffer), { type: "array", cellDates: true });
|
|
1788
|
+
sheetNames = wb.SheetNames || [];
|
|
1789
|
+
} catch (err) {
|
|
1790
|
+
console.warn("[ExcelPlugin] SheetJS parse failed, rendering fallback:", err);
|
|
1544
1791
|
}
|
|
1792
|
+
const tabButtons = [];
|
|
1545
1793
|
const renderSheet = (index) => {
|
|
1794
|
+
if (!wb || index < 1 || index > sheetNames.length) return;
|
|
1795
|
+
currentSheetIndex = index;
|
|
1796
|
+
const sheetName = sheetNames[index - 1];
|
|
1797
|
+
const ws = wb.Sheets[sheetName];
|
|
1546
1798
|
contentArea.innerHTML = "";
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
table
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1799
|
+
if (!ws) {
|
|
1800
|
+
contentArea.innerHTML = '<div style="padding: 24px; color: #888;">Empty sheet</div>';
|
|
1801
|
+
return;
|
|
1802
|
+
}
|
|
1803
|
+
const html = XLSX.utils.sheet_to_html(ws, { id: "fp-sheet-table", editable: false });
|
|
1804
|
+
contentArea.innerHTML = html;
|
|
1805
|
+
const table = contentArea.querySelector("table");
|
|
1806
|
+
if (table) {
|
|
1807
|
+
table.style.borderCollapse = "collapse";
|
|
1808
|
+
table.style.fontFamily = '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif';
|
|
1809
|
+
table.style.fontSize = "13px";
|
|
1810
|
+
table.style.minWidth = "100%";
|
|
1811
|
+
table.style.border = "1px solid #d0d7de";
|
|
1812
|
+
table.style.background = "#fff";
|
|
1813
|
+
table.querySelectorAll("td, th").forEach((cell) => {
|
|
1814
|
+
const el = cell;
|
|
1815
|
+
el.style.border = "1px solid #d0d7de";
|
|
1816
|
+
el.style.padding = "6px 12px";
|
|
1817
|
+
el.style.whiteSpace = "nowrap";
|
|
1562
1818
|
});
|
|
1563
|
-
table.
|
|
1819
|
+
table.querySelectorAll("tr:first-child td, th").forEach((cell) => {
|
|
1820
|
+
const el = cell;
|
|
1821
|
+
el.style.fontWeight = "600";
|
|
1822
|
+
el.style.backgroundColor = "#f6f8fa";
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
tabButtons.forEach((b, i) => {
|
|
1826
|
+
if (i === index - 1) {
|
|
1827
|
+
b.style.backgroundColor = "#2563eb";
|
|
1828
|
+
b.style.color = "#fff";
|
|
1829
|
+
b.style.borderColor = "#2563eb";
|
|
1830
|
+
b.style.fontWeight = "600";
|
|
1831
|
+
} else {
|
|
1832
|
+
b.style.backgroundColor = "#fff";
|
|
1833
|
+
b.style.color = "#333";
|
|
1834
|
+
b.style.borderColor = "#ccc";
|
|
1835
|
+
b.style.fontWeight = "normal";
|
|
1836
|
+
}
|
|
1564
1837
|
});
|
|
1565
|
-
contentArea.appendChild(table);
|
|
1566
|
-
currentSheetIndex = index;
|
|
1567
1838
|
};
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1839
|
+
if (sheetNames.length > 0) {
|
|
1840
|
+
sheetNames.forEach((name, idx) => {
|
|
1841
|
+
const btn = document.createElement("button");
|
|
1842
|
+
btn.textContent = name;
|
|
1843
|
+
btn.style.padding = "4px 12px";
|
|
1844
|
+
btn.style.fontSize = "12px";
|
|
1845
|
+
btn.style.cursor = "pointer";
|
|
1846
|
+
btn.style.border = "1px solid #ccc";
|
|
1847
|
+
btn.style.borderRadius = "4px";
|
|
1848
|
+
btn.style.backgroundColor = "#fff";
|
|
1849
|
+
btn.style.transition = "all 0.15s ease";
|
|
1850
|
+
btn.onclick = () => renderSheet(idx + 1);
|
|
1851
|
+
tabsArea.appendChild(btn);
|
|
1852
|
+
tabButtons.push(btn);
|
|
1853
|
+
});
|
|
1581
1854
|
renderSheet(1);
|
|
1855
|
+
} else {
|
|
1856
|
+
contentArea.innerHTML = `
|
|
1857
|
+
<div style="text-align:center; padding: 40px; color: #666;">
|
|
1858
|
+
<div style="font-size:48px; margin-bottom: 16px;">\u{1F4CA}</div>
|
|
1859
|
+
<h3>${ctx.metadata.name || "Spreadsheet"}</h3>
|
|
1860
|
+
<p>Unable to load sheet data</p>
|
|
1861
|
+
</div>
|
|
1862
|
+
`;
|
|
1582
1863
|
}
|
|
1583
1864
|
const cleanup = () => {
|
|
1584
1865
|
container.remove();
|
|
@@ -1601,11 +1882,11 @@ var ExcelPlugin = class {
|
|
|
1601
1882
|
contentArea.style.transform = `scale(${scale})`;
|
|
1602
1883
|
},
|
|
1603
1884
|
goToPage: (page) => {
|
|
1604
|
-
if (page > 0 && page <=
|
|
1885
|
+
if (page > 0 && page <= sheetNames.length) {
|
|
1605
1886
|
renderSheet(page);
|
|
1606
1887
|
}
|
|
1607
1888
|
},
|
|
1608
|
-
getPageCount: () =>
|
|
1889
|
+
getPageCount: () => sheetNames.length,
|
|
1609
1890
|
getCurrentPage: () => currentSheetIndex,
|
|
1610
1891
|
download: () => {
|
|
1611
1892
|
const blob = new Blob([ctx.buffer], { type: this.mimeTypes[0] });
|
|
@@ -2206,7 +2487,7 @@ var MarkdownPlugin = class {
|
|
|
2206
2487
|
gfm: true,
|
|
2207
2488
|
breaks: true
|
|
2208
2489
|
});
|
|
2209
|
-
const cleanHtml =
|
|
2490
|
+
const cleanHtml = DOMPurify5.sanitize(rawHtml, {
|
|
2210
2491
|
USE_PROFILES: { html: true }
|
|
2211
2492
|
});
|
|
2212
2493
|
const wrapper = document.createElement("div");
|
|
@@ -2345,10 +2626,14 @@ function markdownPlugin() {
|
|
|
2345
2626
|
var PptxPlugin = class {
|
|
2346
2627
|
id = "pptx";
|
|
2347
2628
|
name = "PowerPoint Presentation";
|
|
2348
|
-
extensions = [".pptx", ".ppsx"];
|
|
2629
|
+
extensions = [".pptx", ".ppsx", ".pptm", ".ppsm", ".potx", ".potm"];
|
|
2349
2630
|
mimeTypes = [
|
|
2350
2631
|
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
2351
|
-
"application/vnd.openxmlformats-officedocument.presentationml.slideshow"
|
|
2632
|
+
"application/vnd.openxmlformats-officedocument.presentationml.slideshow",
|
|
2633
|
+
"application/vnd.ms-powerpoint.presentation.macroEnabled.12",
|
|
2634
|
+
"application/vnd.ms-powerpoint.slideshow.macroEnabled.12",
|
|
2635
|
+
"application/vnd.openxmlformats-officedocument.presentationml.template",
|
|
2636
|
+
"application/vnd.ms-powerpoint.template.macroEnabled.12"
|
|
2352
2637
|
];
|
|
2353
2638
|
weight = 80;
|
|
2354
2639
|
supports(file) {
|
|
@@ -2734,6 +3019,1292 @@ var ThreeDPlugin = class {
|
|
|
2734
3019
|
function threeDPlugin() {
|
|
2735
3020
|
return new ThreeDPlugin();
|
|
2736
3021
|
}
|
|
3022
|
+
var RtfPlugin = class {
|
|
3023
|
+
id = "rtf";
|
|
3024
|
+
name = "Rich Text Format (RTF)";
|
|
3025
|
+
extensions = [".rtf"];
|
|
3026
|
+
mimeTypes = ["text/rtf", "application/rtf"];
|
|
3027
|
+
weight = 80;
|
|
3028
|
+
supports(file) {
|
|
3029
|
+
const ext = file.metadata.extension?.toLowerCase();
|
|
3030
|
+
const mime = file.metadata.mimeType?.toLowerCase();
|
|
3031
|
+
return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
|
|
3032
|
+
}
|
|
3033
|
+
getToolbarActions(instance) {
|
|
3034
|
+
return [
|
|
3035
|
+
{
|
|
3036
|
+
id: "zoom-out",
|
|
3037
|
+
icon: "zoom-out",
|
|
3038
|
+
label: "Zoom Out",
|
|
3039
|
+
type: "button",
|
|
3040
|
+
group: "zoom",
|
|
3041
|
+
execute: () => instance.zoomOut?.()
|
|
3042
|
+
},
|
|
3043
|
+
{
|
|
3044
|
+
id: "zoom-in",
|
|
3045
|
+
icon: "zoom-in",
|
|
3046
|
+
label: "Zoom In",
|
|
3047
|
+
type: "button",
|
|
3048
|
+
group: "zoom",
|
|
3049
|
+
execute: () => instance.zoomIn?.()
|
|
3050
|
+
},
|
|
3051
|
+
{
|
|
3052
|
+
id: "fit-page",
|
|
3053
|
+
icon: "fit-page",
|
|
3054
|
+
label: "Fit to Page",
|
|
3055
|
+
type: "button",
|
|
3056
|
+
group: "zoom",
|
|
3057
|
+
execute: () => instance.fitToPage?.()
|
|
3058
|
+
},
|
|
3059
|
+
{
|
|
3060
|
+
id: "download",
|
|
3061
|
+
icon: "download",
|
|
3062
|
+
label: "Download",
|
|
3063
|
+
type: "button",
|
|
3064
|
+
group: "actions",
|
|
3065
|
+
execute: () => instance.download?.()
|
|
3066
|
+
},
|
|
3067
|
+
{
|
|
3068
|
+
id: "print",
|
|
3069
|
+
icon: "print",
|
|
3070
|
+
label: "Print",
|
|
3071
|
+
type: "button",
|
|
3072
|
+
group: "actions",
|
|
3073
|
+
execute: () => instance.print?.()
|
|
3074
|
+
}
|
|
3075
|
+
];
|
|
3076
|
+
}
|
|
3077
|
+
async render(ctx) {
|
|
3078
|
+
const wrapper = document.createElement("div");
|
|
3079
|
+
wrapper.className = "fp-rtf-wrapper";
|
|
3080
|
+
wrapper.style.padding = "32px";
|
|
3081
|
+
wrapper.style.maxWidth = "850px";
|
|
3082
|
+
wrapper.style.margin = "0 auto";
|
|
3083
|
+
wrapper.style.backgroundColor = "#fff";
|
|
3084
|
+
wrapper.style.boxShadow = "0 2px 8px rgba(0,0,0,0.08)";
|
|
3085
|
+
wrapper.style.borderRadius = "4px";
|
|
3086
|
+
wrapper.style.minHeight = "100%";
|
|
3087
|
+
wrapper.style.transformOrigin = "top center";
|
|
3088
|
+
wrapper.style.transition = "transform 0.2s ease";
|
|
3089
|
+
ctx.container.style.overflow = "auto";
|
|
3090
|
+
ctx.container.style.padding = "24px";
|
|
3091
|
+
ctx.container.style.backgroundColor = "#f1f5f9";
|
|
3092
|
+
ctx.container.appendChild(wrapper);
|
|
3093
|
+
let scale = 1;
|
|
3094
|
+
try {
|
|
3095
|
+
const doc = new RTFJS.Document(ctx.buffer, {});
|
|
3096
|
+
const htmlElements = await doc.render();
|
|
3097
|
+
for (const el of htmlElements) {
|
|
3098
|
+
wrapper.appendChild(el);
|
|
3099
|
+
}
|
|
3100
|
+
} catch (err) {
|
|
3101
|
+
console.warn("[RtfPlugin] RTF render error, fallback text:", err);
|
|
3102
|
+
const text = new TextDecoder("latin1").decode(ctx.buffer);
|
|
3103
|
+
const clean = text.replace(/\\par[d]?/g, "\n").replace(/\\[a-zA-Z0-9\-]+/g, "").replace(/[{}]/g, "");
|
|
3104
|
+
wrapper.innerHTML = `<pre style="white-space: pre-wrap; font-family: serif; color: #333;">${clean}</pre>`;
|
|
3105
|
+
}
|
|
3106
|
+
const cleanup = () => {
|
|
3107
|
+
wrapper.remove();
|
|
3108
|
+
ctx.container.innerHTML = "";
|
|
3109
|
+
};
|
|
3110
|
+
ctx.signal.addEventListener("abort", cleanup);
|
|
3111
|
+
return {
|
|
3112
|
+
destroy: cleanup,
|
|
3113
|
+
zoomIn: () => {
|
|
3114
|
+
scale += 0.1;
|
|
3115
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3116
|
+
},
|
|
3117
|
+
zoomOut: () => {
|
|
3118
|
+
scale = Math.max(0.2, scale - 0.1);
|
|
3119
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3120
|
+
},
|
|
3121
|
+
getZoom: () => scale,
|
|
3122
|
+
setZoom: (level) => {
|
|
3123
|
+
scale = level;
|
|
3124
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3125
|
+
},
|
|
3126
|
+
fitToPage: () => {
|
|
3127
|
+
scale = 1;
|
|
3128
|
+
wrapper.style.transform = "scale(1)";
|
|
3129
|
+
},
|
|
3130
|
+
download: () => {
|
|
3131
|
+
const blob = new Blob([ctx.buffer], { type: "application/rtf" });
|
|
3132
|
+
const url = URL.createObjectURL(blob);
|
|
3133
|
+
const a = document.createElement("a");
|
|
3134
|
+
a.href = url;
|
|
3135
|
+
a.download = ctx.metadata.name || "document.rtf";
|
|
3136
|
+
a.click();
|
|
3137
|
+
URL.revokeObjectURL(url);
|
|
3138
|
+
},
|
|
3139
|
+
print: () => {
|
|
3140
|
+
window.print();
|
|
3141
|
+
}
|
|
3142
|
+
};
|
|
3143
|
+
}
|
|
3144
|
+
};
|
|
3145
|
+
function rtfPlugin() {
|
|
3146
|
+
return new RtfPlugin();
|
|
3147
|
+
}
|
|
3148
|
+
var HtmlPreviewPlugin = class {
|
|
3149
|
+
id = "html-preview";
|
|
3150
|
+
name = "HTML Document Preview";
|
|
3151
|
+
extensions = [".html", ".htm", ".xhtml"];
|
|
3152
|
+
mimeTypes = ["text/html", "application/xhtml+xml"];
|
|
3153
|
+
weight = 85;
|
|
3154
|
+
supports(file) {
|
|
3155
|
+
const ext = file.metadata.extension?.toLowerCase();
|
|
3156
|
+
const mime = file.metadata.mimeType?.toLowerCase();
|
|
3157
|
+
return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
|
|
3158
|
+
}
|
|
3159
|
+
getToolbarActions(instance) {
|
|
3160
|
+
return [
|
|
3161
|
+
{
|
|
3162
|
+
id: "zoom-out",
|
|
3163
|
+
icon: "zoom-out",
|
|
3164
|
+
label: "Zoom Out",
|
|
3165
|
+
type: "button",
|
|
3166
|
+
group: "zoom",
|
|
3167
|
+
execute: () => instance.zoomOut?.()
|
|
3168
|
+
},
|
|
3169
|
+
{
|
|
3170
|
+
id: "zoom-in",
|
|
3171
|
+
icon: "zoom-in",
|
|
3172
|
+
label: "Zoom In",
|
|
3173
|
+
type: "button",
|
|
3174
|
+
group: "zoom",
|
|
3175
|
+
execute: () => instance.zoomIn?.()
|
|
3176
|
+
},
|
|
3177
|
+
{
|
|
3178
|
+
id: "fit-page",
|
|
3179
|
+
icon: "fit-page",
|
|
3180
|
+
label: "Fit to Page",
|
|
3181
|
+
type: "button",
|
|
3182
|
+
group: "zoom",
|
|
3183
|
+
execute: () => instance.fitToPage?.()
|
|
3184
|
+
},
|
|
3185
|
+
{
|
|
3186
|
+
id: "copy",
|
|
3187
|
+
icon: "copy",
|
|
3188
|
+
label: "Copy HTML",
|
|
3189
|
+
type: "button",
|
|
3190
|
+
group: "actions",
|
|
3191
|
+
execute: () => instance.copy?.()
|
|
3192
|
+
},
|
|
3193
|
+
{
|
|
3194
|
+
id: "download",
|
|
3195
|
+
icon: "download",
|
|
3196
|
+
label: "Download",
|
|
3197
|
+
type: "button",
|
|
3198
|
+
group: "actions",
|
|
3199
|
+
execute: () => instance.download?.()
|
|
3200
|
+
},
|
|
3201
|
+
{
|
|
3202
|
+
id: "print",
|
|
3203
|
+
icon: "print",
|
|
3204
|
+
label: "Print",
|
|
3205
|
+
type: "button",
|
|
3206
|
+
group: "actions",
|
|
3207
|
+
execute: () => instance.print?.()
|
|
3208
|
+
}
|
|
3209
|
+
];
|
|
3210
|
+
}
|
|
3211
|
+
async render(ctx) {
|
|
3212
|
+
const rawHtml = new TextDecoder("utf-8").decode(ctx.buffer);
|
|
3213
|
+
const sanitized = DOMPurify5.sanitize(rawHtml, {
|
|
3214
|
+
WHOLE_DOCUMENT: true,
|
|
3215
|
+
ADD_TAGS: ["style", "link"],
|
|
3216
|
+
ADD_ATTR: ["target", "rel"]
|
|
3217
|
+
});
|
|
3218
|
+
const iframe = document.createElement("iframe");
|
|
3219
|
+
iframe.className = "fp-html-iframe";
|
|
3220
|
+
iframe.style.width = "100%";
|
|
3221
|
+
iframe.style.height = "100%";
|
|
3222
|
+
iframe.style.border = "none";
|
|
3223
|
+
iframe.style.backgroundColor = "#ffffff";
|
|
3224
|
+
iframe.style.transformOrigin = "top left";
|
|
3225
|
+
iframe.style.transition = "transform 0.2s ease";
|
|
3226
|
+
iframe.sandbox.add("allow-same-origin");
|
|
3227
|
+
ctx.container.style.overflow = "auto";
|
|
3228
|
+
ctx.container.style.width = "100%";
|
|
3229
|
+
ctx.container.style.height = "100%";
|
|
3230
|
+
ctx.container.appendChild(iframe);
|
|
3231
|
+
iframe.srcdoc = sanitized;
|
|
3232
|
+
let scale = 1;
|
|
3233
|
+
const cleanup = () => {
|
|
3234
|
+
iframe.remove();
|
|
3235
|
+
ctx.container.innerHTML = "";
|
|
3236
|
+
};
|
|
3237
|
+
ctx.signal.addEventListener("abort", cleanup);
|
|
3238
|
+
return {
|
|
3239
|
+
destroy: cleanup,
|
|
3240
|
+
zoomIn: () => {
|
|
3241
|
+
scale += 0.1;
|
|
3242
|
+
iframe.style.transform = `scale(${scale})`;
|
|
3243
|
+
},
|
|
3244
|
+
zoomOut: () => {
|
|
3245
|
+
scale = Math.max(0.2, scale - 0.1);
|
|
3246
|
+
iframe.style.transform = `scale(${scale})`;
|
|
3247
|
+
},
|
|
3248
|
+
getZoom: () => scale,
|
|
3249
|
+
setZoom: (level) => {
|
|
3250
|
+
scale = level;
|
|
3251
|
+
iframe.style.transform = `scale(${scale})`;
|
|
3252
|
+
},
|
|
3253
|
+
fitToPage: () => {
|
|
3254
|
+
scale = 1;
|
|
3255
|
+
iframe.style.transform = "scale(1)";
|
|
3256
|
+
},
|
|
3257
|
+
copy: () => {
|
|
3258
|
+
navigator.clipboard.writeText(rawHtml);
|
|
3259
|
+
},
|
|
3260
|
+
download: () => {
|
|
3261
|
+
const blob = new Blob([ctx.buffer], { type: "text/html" });
|
|
3262
|
+
const url = URL.createObjectURL(blob);
|
|
3263
|
+
const a = document.createElement("a");
|
|
3264
|
+
a.href = url;
|
|
3265
|
+
a.download = ctx.metadata.name || "document.html";
|
|
3266
|
+
a.click();
|
|
3267
|
+
URL.revokeObjectURL(url);
|
|
3268
|
+
},
|
|
3269
|
+
print: () => {
|
|
3270
|
+
iframe.contentWindow?.print();
|
|
3271
|
+
}
|
|
3272
|
+
};
|
|
3273
|
+
}
|
|
3274
|
+
};
|
|
3275
|
+
function htmlPreviewPlugin() {
|
|
3276
|
+
return new HtmlPreviewPlugin();
|
|
3277
|
+
}
|
|
3278
|
+
var OpenDocumentPlugin = class {
|
|
3279
|
+
id = "opendocument";
|
|
3280
|
+
name = "OpenDocument Preview (ODT, ODP, ODS, ODG, ODF)";
|
|
3281
|
+
extensions = [".odt", ".odp", ".ods", ".odg", ".odf"];
|
|
3282
|
+
mimeTypes = [
|
|
3283
|
+
"application/vnd.oasis.opendocument.text",
|
|
3284
|
+
"application/vnd.oasis.opendocument.presentation",
|
|
3285
|
+
"application/vnd.oasis.opendocument.spreadsheet",
|
|
3286
|
+
"application/vnd.oasis.opendocument.graphics",
|
|
3287
|
+
"application/vnd.oasis.opendocument.formula"
|
|
3288
|
+
];
|
|
3289
|
+
weight = 80;
|
|
3290
|
+
supports(file) {
|
|
3291
|
+
const ext = file.metadata.extension?.toLowerCase();
|
|
3292
|
+
const mime = file.metadata.mimeType?.toLowerCase();
|
|
3293
|
+
return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
|
|
3294
|
+
}
|
|
3295
|
+
getToolbarActions(instance) {
|
|
3296
|
+
const isPresentation = instance.isPresentation;
|
|
3297
|
+
const actions = [];
|
|
3298
|
+
if (isPresentation) {
|
|
3299
|
+
actions.push(
|
|
3300
|
+
{
|
|
3301
|
+
id: "thumbnails",
|
|
3302
|
+
icon: "thumbnails",
|
|
3303
|
+
label: "Slide Thumbnails",
|
|
3304
|
+
type: "button",
|
|
3305
|
+
group: "navigation",
|
|
3306
|
+
execute: () => instance.toggleThumbnails?.()
|
|
3307
|
+
},
|
|
3308
|
+
{
|
|
3309
|
+
id: "page-prev",
|
|
3310
|
+
icon: "page-prev",
|
|
3311
|
+
label: "Previous Slide",
|
|
3312
|
+
type: "button",
|
|
3313
|
+
group: "navigation",
|
|
3314
|
+
execute: () => {
|
|
3315
|
+
const cur = instance.getCurrentPage?.() ?? 1;
|
|
3316
|
+
if (cur > 1) instance.goToPage?.(cur - 1);
|
|
3317
|
+
}
|
|
3318
|
+
},
|
|
3319
|
+
{
|
|
3320
|
+
id: "page-nav",
|
|
3321
|
+
icon: "",
|
|
3322
|
+
label: "Slide Number",
|
|
3323
|
+
type: "page-nav",
|
|
3324
|
+
group: "navigation",
|
|
3325
|
+
execute: (p) => instance.goToPage?.(Number(p))
|
|
3326
|
+
},
|
|
3327
|
+
{
|
|
3328
|
+
id: "page-next",
|
|
3329
|
+
icon: "page-next",
|
|
3330
|
+
label: "Next Slide",
|
|
3331
|
+
type: "button",
|
|
3332
|
+
group: "navigation",
|
|
3333
|
+
execute: () => {
|
|
3334
|
+
const cur = instance.getCurrentPage?.() ?? 1;
|
|
3335
|
+
const total = instance.getPageCount?.() ?? 1;
|
|
3336
|
+
if (cur < total) instance.goToPage?.(cur + 1);
|
|
3337
|
+
}
|
|
3338
|
+
}
|
|
3339
|
+
);
|
|
3340
|
+
}
|
|
3341
|
+
actions.push(
|
|
3342
|
+
{
|
|
3343
|
+
id: "zoom-out",
|
|
3344
|
+
icon: "zoom-out",
|
|
3345
|
+
label: "Zoom Out",
|
|
3346
|
+
type: "button",
|
|
3347
|
+
group: "zoom",
|
|
3348
|
+
execute: () => instance.zoomOut?.()
|
|
3349
|
+
},
|
|
3350
|
+
{
|
|
3351
|
+
id: "zoom-in",
|
|
3352
|
+
icon: "zoom-in",
|
|
3353
|
+
label: "Zoom In",
|
|
3354
|
+
type: "button",
|
|
3355
|
+
group: "zoom",
|
|
3356
|
+
execute: () => instance.zoomIn?.()
|
|
3357
|
+
},
|
|
3358
|
+
{
|
|
3359
|
+
id: "fit-page",
|
|
3360
|
+
icon: "fit-page",
|
|
3361
|
+
label: "Fit to Page",
|
|
3362
|
+
type: "button",
|
|
3363
|
+
group: "zoom",
|
|
3364
|
+
execute: () => instance.fitToPage?.()
|
|
3365
|
+
},
|
|
3366
|
+
{
|
|
3367
|
+
id: "download",
|
|
3368
|
+
icon: "download",
|
|
3369
|
+
label: "Download",
|
|
3370
|
+
type: "button",
|
|
3371
|
+
group: "actions",
|
|
3372
|
+
execute: () => instance.download?.()
|
|
3373
|
+
},
|
|
3374
|
+
{
|
|
3375
|
+
id: "print",
|
|
3376
|
+
icon: "print",
|
|
3377
|
+
label: "Print",
|
|
3378
|
+
type: "button",
|
|
3379
|
+
group: "actions",
|
|
3380
|
+
execute: () => instance.print?.()
|
|
3381
|
+
}
|
|
3382
|
+
);
|
|
3383
|
+
return actions;
|
|
3384
|
+
}
|
|
3385
|
+
async render(ctx) {
|
|
3386
|
+
const ext = (ctx.metadata.extension || "").toLowerCase();
|
|
3387
|
+
const mime = (ctx.metadata.mimeType || "").toLowerCase();
|
|
3388
|
+
const isPresentation = ext === ".odp" || mime.includes("presentation");
|
|
3389
|
+
const isFormula = ext === ".odf" || mime.includes("formula");
|
|
3390
|
+
ext === ".odg" || mime.includes("graphics");
|
|
3391
|
+
let unzipped;
|
|
3392
|
+
try {
|
|
3393
|
+
unzipped = unzipSync(new Uint8Array(ctx.buffer));
|
|
3394
|
+
} catch {
|
|
3395
|
+
throw new Error("Failed to decompress OpenDocument package (invalid ZIP format)");
|
|
3396
|
+
}
|
|
3397
|
+
const imageUrls = /* @__PURE__ */ new Map();
|
|
3398
|
+
for (const [filePath, fileBytes] of Object.entries(unzipped)) {
|
|
3399
|
+
if (filePath.startsWith("Pictures/")) {
|
|
3400
|
+
const imageMime = filePath.endsWith(".png") ? "image/png" : filePath.endsWith(".jpg") || filePath.endsWith(".jpeg") ? "image/jpeg" : filePath.endsWith(".svg") ? "image/svg+xml" : "application/octet-stream";
|
|
3401
|
+
const blob = new Blob([fileBytes], { type: imageMime });
|
|
3402
|
+
imageUrls.set(filePath, URL.createObjectURL(blob));
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
const contentXmlStr = unzipped["content.xml"] ? strFromU8(unzipped["content.xml"]) : "";
|
|
3406
|
+
const stylesXmlStr = unzipped["styles.xml"] ? strFromU8(unzipped["styles.xml"]) : "";
|
|
3407
|
+
const parser = new DOMParser();
|
|
3408
|
+
const contentDoc = parser.parseFromString(contentXmlStr, "application/xml");
|
|
3409
|
+
const stylesDoc = stylesXmlStr ? parser.parseFromString(stylesXmlStr, "application/xml") : null;
|
|
3410
|
+
const styleMap = this.extractStyles(stylesDoc, contentDoc);
|
|
3411
|
+
const container = document.createElement("div");
|
|
3412
|
+
container.className = "fp-odf-container";
|
|
3413
|
+
container.style.width = "100%";
|
|
3414
|
+
container.style.height = "100%";
|
|
3415
|
+
container.style.overflow = "auto";
|
|
3416
|
+
container.style.padding = "24px";
|
|
3417
|
+
container.style.backgroundColor = "#f1f5f9";
|
|
3418
|
+
const wrapper = document.createElement("div");
|
|
3419
|
+
wrapper.className = "fp-odf-wrapper";
|
|
3420
|
+
wrapper.style.margin = "0 auto";
|
|
3421
|
+
wrapper.style.backgroundColor = "#ffffff";
|
|
3422
|
+
wrapper.style.boxShadow = "0 2px 10px rgba(0,0,0,0.08)";
|
|
3423
|
+
wrapper.style.borderRadius = "4px";
|
|
3424
|
+
wrapper.style.transformOrigin = "top center";
|
|
3425
|
+
wrapper.style.transition = "transform 0.2s ease";
|
|
3426
|
+
container.appendChild(wrapper);
|
|
3427
|
+
ctx.container.appendChild(container);
|
|
3428
|
+
let scale = 1;
|
|
3429
|
+
let currentPage = 1;
|
|
3430
|
+
let totalPages = 1;
|
|
3431
|
+
let slides = [];
|
|
3432
|
+
if (isPresentation) {
|
|
3433
|
+
wrapper.style.maxWidth = "960px";
|
|
3434
|
+
wrapper.style.aspectRatio = "16 / 9";
|
|
3435
|
+
wrapper.style.position = "relative";
|
|
3436
|
+
wrapper.style.overflow = "hidden";
|
|
3437
|
+
slides = this.renderSlides(contentDoc, styleMap, imageUrls);
|
|
3438
|
+
totalPages = Math.max(1, slides.length);
|
|
3439
|
+
slides.forEach((slide, idx) => {
|
|
3440
|
+
slide.style.display = idx === 0 ? "block" : "none";
|
|
3441
|
+
slide.style.width = "100%";
|
|
3442
|
+
slide.style.height = "100%";
|
|
3443
|
+
slide.style.position = "absolute";
|
|
3444
|
+
slide.style.top = "0";
|
|
3445
|
+
slide.style.left = "0";
|
|
3446
|
+
wrapper.appendChild(slide);
|
|
3447
|
+
});
|
|
3448
|
+
} else if (isFormula) {
|
|
3449
|
+
wrapper.style.maxWidth = "800px";
|
|
3450
|
+
wrapper.style.padding = "48px";
|
|
3451
|
+
wrapper.style.textAlign = "center";
|
|
3452
|
+
wrapper.style.fontSize = "24px";
|
|
3453
|
+
const mathEl = contentDoc.querySelector("math");
|
|
3454
|
+
if (mathEl) {
|
|
3455
|
+
wrapper.innerHTML = mathEl.outerHTML;
|
|
3456
|
+
} else {
|
|
3457
|
+
wrapper.textContent = contentDoc.documentElement.textContent || "Formula content";
|
|
3458
|
+
}
|
|
3459
|
+
} else {
|
|
3460
|
+
wrapper.style.maxWidth = "850px";
|
|
3461
|
+
wrapper.style.padding = "48px";
|
|
3462
|
+
wrapper.style.minHeight = "100%";
|
|
3463
|
+
const bodyHtml = this.renderOdfBody(contentDoc, styleMap, imageUrls);
|
|
3464
|
+
wrapper.innerHTML = DOMPurify5.sanitize(bodyHtml, {
|
|
3465
|
+
ADD_TAGS: ["math", "semantics", "mrow", "mi", "mo", "mn", "msup", "msub"],
|
|
3466
|
+
ADD_ATTR: ["style", "colspan", "rowspan"]
|
|
3467
|
+
});
|
|
3468
|
+
}
|
|
3469
|
+
const cleanup = () => {
|
|
3470
|
+
imageUrls.forEach((url) => URL.revokeObjectURL(url));
|
|
3471
|
+
container.remove();
|
|
3472
|
+
ctx.container.innerHTML = "";
|
|
3473
|
+
};
|
|
3474
|
+
ctx.signal.addEventListener("abort", cleanup);
|
|
3475
|
+
const goToPage = (page) => {
|
|
3476
|
+
if (!isPresentation || page < 1 || page > totalPages) return;
|
|
3477
|
+
currentPage = page;
|
|
3478
|
+
slides.forEach((s, idx) => {
|
|
3479
|
+
s.style.display = idx === page - 1 ? "block" : "none";
|
|
3480
|
+
});
|
|
3481
|
+
ctx.emit("page-change", { page: currentPage, total: totalPages });
|
|
3482
|
+
};
|
|
3483
|
+
return {
|
|
3484
|
+
destroy: cleanup,
|
|
3485
|
+
isPresentation,
|
|
3486
|
+
zoomIn: () => {
|
|
3487
|
+
scale += 0.1;
|
|
3488
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3489
|
+
},
|
|
3490
|
+
zoomOut: () => {
|
|
3491
|
+
scale = Math.max(0.2, scale - 0.1);
|
|
3492
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3493
|
+
},
|
|
3494
|
+
getZoom: () => scale,
|
|
3495
|
+
setZoom: (level) => {
|
|
3496
|
+
scale = level;
|
|
3497
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3498
|
+
},
|
|
3499
|
+
fitToPage: () => {
|
|
3500
|
+
scale = 1;
|
|
3501
|
+
wrapper.style.transform = "scale(1)";
|
|
3502
|
+
},
|
|
3503
|
+
goToPage,
|
|
3504
|
+
getPageCount: () => totalPages,
|
|
3505
|
+
getCurrentPage: () => currentPage,
|
|
3506
|
+
getThumbnails: async () => {
|
|
3507
|
+
if (!isPresentation) return [];
|
|
3508
|
+
return slides.map((_, idx) => ({
|
|
3509
|
+
index: idx,
|
|
3510
|
+
label: `Slide ${idx + 1}`,
|
|
3511
|
+
render: async (canvas) => {
|
|
3512
|
+
const ctx2d = canvas.getContext("2d");
|
|
3513
|
+
if (!ctx2d) return;
|
|
3514
|
+
canvas.width = 160;
|
|
3515
|
+
canvas.height = 90;
|
|
3516
|
+
ctx2d.fillStyle = "#f8fafc";
|
|
3517
|
+
ctx2d.fillRect(0, 0, 160, 90);
|
|
3518
|
+
ctx2d.fillStyle = "#334155";
|
|
3519
|
+
ctx2d.font = "bold 12px sans-serif";
|
|
3520
|
+
ctx2d.textAlign = "center";
|
|
3521
|
+
ctx2d.fillText(`Slide ${idx + 1}`, 80, 50);
|
|
3522
|
+
}
|
|
3523
|
+
}));
|
|
3524
|
+
},
|
|
3525
|
+
download: () => {
|
|
3526
|
+
const mimeType = this.mimeTypes.find((m) => m.includes(ext.replace(".", ""))) || "application/octet-stream";
|
|
3527
|
+
const blob = new Blob([ctx.buffer], { type: mimeType });
|
|
3528
|
+
const url = URL.createObjectURL(blob);
|
|
3529
|
+
const a = document.createElement("a");
|
|
3530
|
+
a.href = url;
|
|
3531
|
+
a.download = ctx.metadata.name || `document${ext}`;
|
|
3532
|
+
a.click();
|
|
3533
|
+
URL.revokeObjectURL(url);
|
|
3534
|
+
},
|
|
3535
|
+
print: () => {
|
|
3536
|
+
window.print();
|
|
3537
|
+
}
|
|
3538
|
+
};
|
|
3539
|
+
}
|
|
3540
|
+
extractStyles(stylesDoc, contentDoc) {
|
|
3541
|
+
const map = /* @__PURE__ */ new Map();
|
|
3542
|
+
const styleNodes = [];
|
|
3543
|
+
if (stylesDoc) {
|
|
3544
|
+
styleNodes.push(...Array.from(stylesDoc.querySelectorAll("style, [name]")));
|
|
3545
|
+
}
|
|
3546
|
+
styleNodes.push(...Array.from(contentDoc.querySelectorAll("style, [name]")));
|
|
3547
|
+
for (const node of styleNodes) {
|
|
3548
|
+
const name = node.getAttribute("style:name") || node.getAttribute("name");
|
|
3549
|
+
if (!name) continue;
|
|
3550
|
+
let css = "";
|
|
3551
|
+
const textProp = node.querySelector("text-properties, [font-weight], [font-style], [color]");
|
|
3552
|
+
if (textProp) {
|
|
3553
|
+
const weight = textProp.getAttribute("fo:font-weight") || textProp.getAttribute("font-weight");
|
|
3554
|
+
const style = textProp.getAttribute("fo:font-style") || textProp.getAttribute("font-style");
|
|
3555
|
+
const color = textProp.getAttribute("fo:color") || textProp.getAttribute("color");
|
|
3556
|
+
const size = textProp.getAttribute("fo:font-size") || textProp.getAttribute("font-size");
|
|
3557
|
+
if (weight === "bold") css += "font-weight: bold; ";
|
|
3558
|
+
if (style === "italic") css += "font-style: italic; ";
|
|
3559
|
+
if (color) css += `color: ${color}; `;
|
|
3560
|
+
if (size) css += `font-size: ${size}; `;
|
|
3561
|
+
}
|
|
3562
|
+
const paraProp = node.querySelector("paragraph-properties, [text-align]");
|
|
3563
|
+
if (paraProp) {
|
|
3564
|
+
const align = paraProp.getAttribute("fo:text-align") || paraProp.getAttribute("text-align");
|
|
3565
|
+
const mt = paraProp.getAttribute("fo:margin-top") || paraProp.getAttribute("margin-top");
|
|
3566
|
+
const mb = paraProp.getAttribute("fo:margin-bottom") || paraProp.getAttribute("margin-bottom");
|
|
3567
|
+
if (align) css += `text-align: ${align}; `;
|
|
3568
|
+
if (mt) css += `margin-top: ${mt}; `;
|
|
3569
|
+
if (mb) css += `margin-bottom: ${mb}; `;
|
|
3570
|
+
}
|
|
3571
|
+
if (css) map.set(name, css);
|
|
3572
|
+
}
|
|
3573
|
+
return map;
|
|
3574
|
+
}
|
|
3575
|
+
renderOdfBody(contentDoc, styles, images) {
|
|
3576
|
+
const body = contentDoc.querySelector("body") || contentDoc.documentElement;
|
|
3577
|
+
let html = "";
|
|
3578
|
+
const walk = (node) => {
|
|
3579
|
+
const tag = node.localName || node.tagName.toLowerCase();
|
|
3580
|
+
switch (tag) {
|
|
3581
|
+
case "h": {
|
|
3582
|
+
const level = Math.min(6, Math.max(1, Number(node.getAttribute("text:outline-level") || 1)));
|
|
3583
|
+
const style = styles.get(node.getAttribute("text:style-name") || "") || "";
|
|
3584
|
+
html += `<h${level} style="${style}; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${node.textContent || ""}</h${level}>`;
|
|
3585
|
+
break;
|
|
3586
|
+
}
|
|
3587
|
+
case "p": {
|
|
3588
|
+
const style = styles.get(node.getAttribute("text:style-name") || "") || "";
|
|
3589
|
+
const inner = this.renderSpans(node, styles, images);
|
|
3590
|
+
html += `<p style="${style}; line-height: 1.6; margin: 8px 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${inner}</p>`;
|
|
3591
|
+
break;
|
|
3592
|
+
}
|
|
3593
|
+
case "list": {
|
|
3594
|
+
html += '<ul style="margin: 8px 0; padding-left: 24px;">';
|
|
3595
|
+
Array.from(node.children).forEach((c) => walk(c));
|
|
3596
|
+
html += "</ul>";
|
|
3597
|
+
break;
|
|
3598
|
+
}
|
|
3599
|
+
case "list-item": {
|
|
3600
|
+
html += "<li>";
|
|
3601
|
+
Array.from(node.children).forEach((c) => walk(c));
|
|
3602
|
+
html += "</li>";
|
|
3603
|
+
break;
|
|
3604
|
+
}
|
|
3605
|
+
case "table": {
|
|
3606
|
+
html += '<table style="border-collapse: collapse; width: 100%; margin: 16px 0; border: 1px solid #d0d7de;">';
|
|
3607
|
+
Array.from(node.children).forEach((c) => walk(c));
|
|
3608
|
+
html += "</table>";
|
|
3609
|
+
break;
|
|
3610
|
+
}
|
|
3611
|
+
case "table-row": {
|
|
3612
|
+
html += "<tr>";
|
|
3613
|
+
Array.from(node.children).forEach((c) => walk(c));
|
|
3614
|
+
html += "</tr>";
|
|
3615
|
+
break;
|
|
3616
|
+
}
|
|
3617
|
+
case "table-cell": {
|
|
3618
|
+
html += '<td style="border: 1px solid #d0d7de; padding: 8px 12px; font-size: 13px;">';
|
|
3619
|
+
Array.from(node.children).forEach((c) => walk(c));
|
|
3620
|
+
html += "</td>";
|
|
3621
|
+
break;
|
|
3622
|
+
}
|
|
3623
|
+
default:
|
|
3624
|
+
Array.from(node.children).forEach((c) => walk(c));
|
|
3625
|
+
}
|
|
3626
|
+
};
|
|
3627
|
+
Array.from(body.children).forEach((c) => walk(c));
|
|
3628
|
+
return html || '<p style="color: #666; font-style: italic;">(Document is empty)</p>';
|
|
3629
|
+
}
|
|
3630
|
+
renderSpans(node, styles, images) {
|
|
3631
|
+
let result = "";
|
|
3632
|
+
for (const child of Array.from(node.childNodes)) {
|
|
3633
|
+
if (child.nodeType === Node.TEXT_NODE) {
|
|
3634
|
+
result += child.textContent || "";
|
|
3635
|
+
} else if (child.nodeType === Node.ELEMENT_NODE) {
|
|
3636
|
+
const el = child;
|
|
3637
|
+
const tag = el.localName || el.tagName.toLowerCase();
|
|
3638
|
+
if (tag === "span") {
|
|
3639
|
+
const style = styles.get(el.getAttribute("text:style-name") || "") || "";
|
|
3640
|
+
result += `<span style="${style}">${el.textContent || ""}</span>`;
|
|
3641
|
+
} else if (tag === "image") {
|
|
3642
|
+
const href = el.getAttribute("xlink:href") || el.getAttribute("href") || "";
|
|
3643
|
+
const blobUrl = images.get(href);
|
|
3644
|
+
if (blobUrl) {
|
|
3645
|
+
result += `<img src="${blobUrl}" style="max-width: 100%; height: auto; margin: 8px 0;" alt="Embedded image" />`;
|
|
3646
|
+
}
|
|
3647
|
+
} else if (tag === "s") {
|
|
3648
|
+
const count = Number(el.getAttribute("text:c") || 1);
|
|
3649
|
+
result += " ".repeat(count);
|
|
3650
|
+
} else if (tag === "tab") {
|
|
3651
|
+
result += " ";
|
|
3652
|
+
} else if (tag === "line-break") {
|
|
3653
|
+
result += "<br/>";
|
|
3654
|
+
} else {
|
|
3655
|
+
result += el.textContent || "";
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
}
|
|
3659
|
+
return result;
|
|
3660
|
+
}
|
|
3661
|
+
renderSlides(contentDoc, styles, images) {
|
|
3662
|
+
const pages = Array.from(contentDoc.querySelectorAll("page, [draw\\:name]"));
|
|
3663
|
+
const slides = [];
|
|
3664
|
+
const pageNodes = pages.length > 0 ? pages : Array.from(contentDoc.getElementsByTagNameNS("*", "page"));
|
|
3665
|
+
if (pageNodes.length === 0) {
|
|
3666
|
+
const fallbackSlide = document.createElement("div");
|
|
3667
|
+
fallbackSlide.style.padding = "40px";
|
|
3668
|
+
fallbackSlide.style.textAlign = "center";
|
|
3669
|
+
fallbackSlide.innerHTML = "<h2>Presentation</h2><p>No slides found</p>";
|
|
3670
|
+
return [fallbackSlide];
|
|
3671
|
+
}
|
|
3672
|
+
pageNodes.forEach((pageEl, idx) => {
|
|
3673
|
+
const slide = document.createElement("div");
|
|
3674
|
+
slide.className = `fp-odp-slide fp-odp-slide-${idx + 1}`;
|
|
3675
|
+
slide.style.padding = "40px";
|
|
3676
|
+
slide.style.boxSizing = "border-box";
|
|
3677
|
+
slide.style.display = "flex";
|
|
3678
|
+
slide.style.flexDirection = "column";
|
|
3679
|
+
slide.style.justifyContent = "center";
|
|
3680
|
+
slide.style.alignItems = "center";
|
|
3681
|
+
slide.style.background = "#ffffff";
|
|
3682
|
+
const textFrames = Array.from(pageEl.querySelectorAll("frame, text-box, [draw\\:text-style-name]"));
|
|
3683
|
+
if (textFrames.length > 0) {
|
|
3684
|
+
textFrames.forEach((frame) => {
|
|
3685
|
+
const text = frame.textContent?.trim();
|
|
3686
|
+
if (text) {
|
|
3687
|
+
const p = document.createElement("div");
|
|
3688
|
+
p.style.margin = "12px 0";
|
|
3689
|
+
p.style.fontSize = idx === 0 ? "24px" : "16px";
|
|
3690
|
+
p.style.fontWeight = idx === 0 ? "bold" : "normal";
|
|
3691
|
+
p.style.color = "#1e293b";
|
|
3692
|
+
p.textContent = text;
|
|
3693
|
+
slide.appendChild(p);
|
|
3694
|
+
}
|
|
3695
|
+
});
|
|
3696
|
+
} else {
|
|
3697
|
+
slide.innerHTML = `<h3>Slide ${idx + 1}</h3><p>${pageEl.textContent?.trim() || ""}</p>`;
|
|
3698
|
+
}
|
|
3699
|
+
slides.push(slide);
|
|
3700
|
+
});
|
|
3701
|
+
return slides;
|
|
3702
|
+
}
|
|
3703
|
+
};
|
|
3704
|
+
function openDocumentPlugin() {
|
|
3705
|
+
return new OpenDocumentPlugin();
|
|
3706
|
+
}
|
|
3707
|
+
var DocPlugin = class {
|
|
3708
|
+
id = "doc";
|
|
3709
|
+
name = "Legacy Word Document Preview (.doc, .dot)";
|
|
3710
|
+
extensions = [".doc", ".dot"];
|
|
3711
|
+
mimeTypes = ["application/msword", "application/vnd.ms-word"];
|
|
3712
|
+
weight = 75;
|
|
3713
|
+
supports(file) {
|
|
3714
|
+
const ext = file.metadata.extension?.toLowerCase();
|
|
3715
|
+
const mime = file.metadata.mimeType?.toLowerCase();
|
|
3716
|
+
return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
|
|
3717
|
+
}
|
|
3718
|
+
getToolbarActions(instance) {
|
|
3719
|
+
return [
|
|
3720
|
+
{
|
|
3721
|
+
id: "zoom-out",
|
|
3722
|
+
icon: "zoom-out",
|
|
3723
|
+
label: "Zoom Out",
|
|
3724
|
+
type: "button",
|
|
3725
|
+
group: "zoom",
|
|
3726
|
+
execute: () => instance.zoomOut?.()
|
|
3727
|
+
},
|
|
3728
|
+
{
|
|
3729
|
+
id: "zoom-in",
|
|
3730
|
+
icon: "zoom-in",
|
|
3731
|
+
label: "Zoom In",
|
|
3732
|
+
type: "button",
|
|
3733
|
+
group: "zoom",
|
|
3734
|
+
execute: () => instance.zoomIn?.()
|
|
3735
|
+
},
|
|
3736
|
+
{
|
|
3737
|
+
id: "fit-page",
|
|
3738
|
+
icon: "fit-page",
|
|
3739
|
+
label: "Fit to Page",
|
|
3740
|
+
type: "button",
|
|
3741
|
+
group: "zoom",
|
|
3742
|
+
execute: () => instance.fitToPage?.()
|
|
3743
|
+
},
|
|
3744
|
+
{
|
|
3745
|
+
id: "copy",
|
|
3746
|
+
icon: "copy",
|
|
3747
|
+
label: "Copy Text",
|
|
3748
|
+
type: "button",
|
|
3749
|
+
group: "actions",
|
|
3750
|
+
execute: () => instance.copy?.()
|
|
3751
|
+
},
|
|
3752
|
+
{
|
|
3753
|
+
id: "download",
|
|
3754
|
+
icon: "download",
|
|
3755
|
+
label: "Download",
|
|
3756
|
+
type: "button",
|
|
3757
|
+
group: "actions",
|
|
3758
|
+
execute: () => instance.download?.()
|
|
3759
|
+
},
|
|
3760
|
+
{
|
|
3761
|
+
id: "print",
|
|
3762
|
+
icon: "print",
|
|
3763
|
+
label: "Print",
|
|
3764
|
+
type: "button",
|
|
3765
|
+
group: "actions",
|
|
3766
|
+
execute: () => instance.print?.()
|
|
3767
|
+
}
|
|
3768
|
+
];
|
|
3769
|
+
}
|
|
3770
|
+
async render(ctx) {
|
|
3771
|
+
const container = document.createElement("div");
|
|
3772
|
+
container.className = "fp-doc-container";
|
|
3773
|
+
container.style.width = "100%";
|
|
3774
|
+
container.style.height = "100%";
|
|
3775
|
+
container.style.overflow = "auto";
|
|
3776
|
+
container.style.padding = "32px 16px";
|
|
3777
|
+
container.style.backgroundColor = "#f1f5f9";
|
|
3778
|
+
const wrapper = document.createElement("div");
|
|
3779
|
+
wrapper.className = "fp-doc-wrapper";
|
|
3780
|
+
wrapper.style.maxWidth = "850px";
|
|
3781
|
+
wrapper.style.margin = "0 auto";
|
|
3782
|
+
wrapper.style.backgroundColor = "#ffffff";
|
|
3783
|
+
wrapper.style.boxShadow = "0 2px 12px rgba(0,0,0,0.08)";
|
|
3784
|
+
wrapper.style.borderRadius = "4px";
|
|
3785
|
+
wrapper.style.padding = "56px 48px";
|
|
3786
|
+
wrapper.style.minHeight = "100%";
|
|
3787
|
+
wrapper.style.transformOrigin = "top center";
|
|
3788
|
+
wrapper.style.transition = "transform 0.2s ease";
|
|
3789
|
+
wrapper.style.fontFamily = 'Calibri, "Segoe UI", Arial, sans-serif';
|
|
3790
|
+
wrapper.style.color = "#1e293b";
|
|
3791
|
+
container.appendChild(wrapper);
|
|
3792
|
+
ctx.container.appendChild(container);
|
|
3793
|
+
let scale = 1;
|
|
3794
|
+
let extractedRawText = "";
|
|
3795
|
+
try {
|
|
3796
|
+
const cfbf = new CfbfReader(ctx.buffer);
|
|
3797
|
+
const wordDocStream = cfbf.readStream("WordDocument");
|
|
3798
|
+
if (!wordDocStream || wordDocStream.length < 512) {
|
|
3799
|
+
throw new Error("WordDocument stream not found or invalid in CFBF archive");
|
|
3800
|
+
}
|
|
3801
|
+
const view = new DataView(wordDocStream.buffer, wordDocStream.byteOffset, wordDocStream.byteLength);
|
|
3802
|
+
const flags = view.getUint16(10, true);
|
|
3803
|
+
const is1Table = (flags & 512) !== 0;
|
|
3804
|
+
const tableName = is1Table ? "1Table" : "0Table";
|
|
3805
|
+
const tableStream = cfbf.readStream(tableName);
|
|
3806
|
+
const text = this.extractDocText(wordDocStream, tableStream);
|
|
3807
|
+
extractedRawText = text;
|
|
3808
|
+
wrapper.innerHTML = this.formatDocToHtml(text, ctx.metadata.name || "Document");
|
|
3809
|
+
} catch (err) {
|
|
3810
|
+
console.warn("[DocPlugin] Binary parsing error, fallback text:", err);
|
|
3811
|
+
const fallback = this.heuristicTextExtraction(ctx.buffer);
|
|
3812
|
+
extractedRawText = fallback;
|
|
3813
|
+
wrapper.innerHTML = `
|
|
3814
|
+
<div style="border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 24px;">
|
|
3815
|
+
<h2 style="margin: 0 0 6px; font-size: 20px; color: #334155;">${ctx.metadata.name || "Word Document (.doc)"}</h2>
|
|
3816
|
+
<span style="font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px;">Legacy Word 97-2003 Binary Preview</span>
|
|
3817
|
+
</div>
|
|
3818
|
+
${this.formatDocToHtml(fallback, ctx.metadata.name || "Document")}
|
|
3819
|
+
`;
|
|
3820
|
+
}
|
|
3821
|
+
const cleanup = () => {
|
|
3822
|
+
container.remove();
|
|
3823
|
+
ctx.container.innerHTML = "";
|
|
3824
|
+
};
|
|
3825
|
+
ctx.signal.addEventListener("abort", cleanup);
|
|
3826
|
+
return {
|
|
3827
|
+
destroy: cleanup,
|
|
3828
|
+
zoomIn: () => {
|
|
3829
|
+
scale += 0.1;
|
|
3830
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3831
|
+
},
|
|
3832
|
+
zoomOut: () => {
|
|
3833
|
+
scale = Math.max(0.2, scale - 0.1);
|
|
3834
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3835
|
+
},
|
|
3836
|
+
getZoom: () => scale,
|
|
3837
|
+
setZoom: (level) => {
|
|
3838
|
+
scale = level;
|
|
3839
|
+
wrapper.style.transform = `scale(${scale})`;
|
|
3840
|
+
},
|
|
3841
|
+
fitToPage: () => {
|
|
3842
|
+
scale = 1;
|
|
3843
|
+
wrapper.style.transform = "scale(1)";
|
|
3844
|
+
},
|
|
3845
|
+
copy: () => {
|
|
3846
|
+
navigator.clipboard.writeText(extractedRawText);
|
|
3847
|
+
},
|
|
3848
|
+
download: () => {
|
|
3849
|
+
const blob = new Blob([ctx.buffer], { type: "application/msword" });
|
|
3850
|
+
const url = URL.createObjectURL(blob);
|
|
3851
|
+
const a = document.createElement("a");
|
|
3852
|
+
a.href = url;
|
|
3853
|
+
a.download = ctx.metadata.name || "document.doc";
|
|
3854
|
+
a.click();
|
|
3855
|
+
URL.revokeObjectURL(url);
|
|
3856
|
+
},
|
|
3857
|
+
print: () => {
|
|
3858
|
+
window.print();
|
|
3859
|
+
}
|
|
3860
|
+
};
|
|
3861
|
+
}
|
|
3862
|
+
/**
|
|
3863
|
+
* Extract document text from WordDocument stream and Table stream
|
|
3864
|
+
*/
|
|
3865
|
+
extractDocText(wordDoc, tableStream) {
|
|
3866
|
+
const view = new DataView(wordDoc.buffer, wordDoc.byteOffset, wordDoc.byteLength);
|
|
3867
|
+
let fcClx = 0;
|
|
3868
|
+
let lcbClx = 0;
|
|
3869
|
+
if (wordDoc.length >= 426) {
|
|
3870
|
+
fcClx = view.getUint32(418, true);
|
|
3871
|
+
lcbClx = view.getUint32(422, true);
|
|
3872
|
+
}
|
|
3873
|
+
if (tableStream && fcClx > 0 && lcbClx > 0 && fcClx + lcbClx <= tableStream.length) {
|
|
3874
|
+
try {
|
|
3875
|
+
const text = this.parsePieceTable(wordDoc, tableStream, fcClx, lcbClx);
|
|
3876
|
+
if (text && text.trim().length > 0) return text;
|
|
3877
|
+
} catch (err) {
|
|
3878
|
+
console.warn("[DocPlugin] Error in piece table parsing:", err);
|
|
3879
|
+
}
|
|
3880
|
+
}
|
|
3881
|
+
if (wordDoc.length > 2560) {
|
|
3882
|
+
const textChunk = wordDoc.subarray(2560);
|
|
3883
|
+
const extracted = this.extractStringsFromBytes(textChunk);
|
|
3884
|
+
if (extracted.trim().length > 0) return extracted;
|
|
3885
|
+
}
|
|
3886
|
+
return this.extractStringsFromBytes(wordDoc);
|
|
3887
|
+
}
|
|
3888
|
+
/**
|
|
3889
|
+
* Parse the CLX and Piece Table (Plcfpcd) according to [MS-DOC]
|
|
3890
|
+
*/
|
|
3891
|
+
parsePieceTable(wordDoc, table, fcClx, lcbClx) {
|
|
3892
|
+
let offset = fcClx;
|
|
3893
|
+
const end = fcClx + lcbClx;
|
|
3894
|
+
while (offset < end) {
|
|
3895
|
+
const clxt = table[offset];
|
|
3896
|
+
if (clxt === 1) {
|
|
3897
|
+
const cb = new DataView(table.buffer, table.byteOffset + offset + 1).getUint16(0, true);
|
|
3898
|
+
offset += 3 + cb;
|
|
3899
|
+
} else if (clxt === 2) {
|
|
3900
|
+
offset += 1;
|
|
3901
|
+
const lcb = new DataView(table.buffer, table.byteOffset + offset).getUint32(0, true);
|
|
3902
|
+
offset += 4;
|
|
3903
|
+
return this.readPlcfpcd(wordDoc, table, offset, lcb);
|
|
3904
|
+
} else {
|
|
3905
|
+
break;
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
return "";
|
|
3909
|
+
}
|
|
3910
|
+
readPlcfpcd(wordDoc, table, offset, lcb) {
|
|
3911
|
+
const view = new DataView(table.buffer, table.byteOffset + offset);
|
|
3912
|
+
const n = Math.floor((lcb - 4) / 12);
|
|
3913
|
+
if (n <= 0) return "";
|
|
3914
|
+
const cpOffsets = [];
|
|
3915
|
+
for (let i = 0; i <= n; i++) {
|
|
3916
|
+
cpOffsets.push(view.getUint32(i * 4, true));
|
|
3917
|
+
}
|
|
3918
|
+
const pcdOffset = (n + 1) * 4;
|
|
3919
|
+
let fullText = "";
|
|
3920
|
+
for (let i = 0; i < n; i++) {
|
|
3921
|
+
const fc = view.getUint32(pcdOffset + i * 8 + 2, true);
|
|
3922
|
+
const isCompressed = (fc & 1073741824) !== 0;
|
|
3923
|
+
const byteOffset = (fc & 1073741823) >> (isCompressed ? 1 : 0);
|
|
3924
|
+
const charCount = cpOffsets[i + 1] - cpOffsets[i];
|
|
3925
|
+
if (isCompressed) {
|
|
3926
|
+
const slice = wordDoc.subarray(byteOffset, byteOffset + charCount);
|
|
3927
|
+
fullText += new TextDecoder("latin1").decode(slice);
|
|
3928
|
+
} else {
|
|
3929
|
+
const slice = wordDoc.subarray(byteOffset, byteOffset + charCount * 2);
|
|
3930
|
+
fullText += new TextDecoder("utf-16le").decode(slice);
|
|
3931
|
+
}
|
|
3932
|
+
}
|
|
3933
|
+
return fullText;
|
|
3934
|
+
}
|
|
3935
|
+
/**
|
|
3936
|
+
* Scans a byte array for continuous sequences of readable characters (ANSI and UTF-16LE)
|
|
3937
|
+
*/
|
|
3938
|
+
extractStringsFromBytes(bytes) {
|
|
3939
|
+
const chars = [];
|
|
3940
|
+
const len = bytes.length;
|
|
3941
|
+
for (let i = 0; i < len; i++) {
|
|
3942
|
+
const b = bytes[i];
|
|
3943
|
+
if (b === 13 || b === 10 || b === 9 || b >= 32 && b <= 126 || b >= 160 && b <= 255) {
|
|
3944
|
+
chars.push(String.fromCharCode(b));
|
|
3945
|
+
} else if (b === 0 && i + 1 < len && bytes[i + 1] >= 32 && bytes[i + 1] <= 126) {
|
|
3946
|
+
chars.push(String.fromCharCode(bytes[i + 1]));
|
|
3947
|
+
i++;
|
|
3948
|
+
} else if (b === 7) {
|
|
3949
|
+
chars.push(" ");
|
|
3950
|
+
} else if (b === 12) {
|
|
3951
|
+
chars.push("\n\n---PAGE---\n\n");
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
return chars.join("");
|
|
3955
|
+
}
|
|
3956
|
+
heuristicTextExtraction(buffer) {
|
|
3957
|
+
return this.extractStringsFromBytes(new Uint8Array(buffer));
|
|
3958
|
+
}
|
|
3959
|
+
/**
|
|
3960
|
+
* Format cleaned extracted text into attractive HTML paragraphs, headings, and lists
|
|
3961
|
+
*/
|
|
3962
|
+
formatDocToHtml(text, filename) {
|
|
3963
|
+
const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x0C/g, "\n\n").split("\n");
|
|
3964
|
+
let html = "";
|
|
3965
|
+
let inList = false;
|
|
3966
|
+
for (const rawLine of lines) {
|
|
3967
|
+
const line = rawLine.trim();
|
|
3968
|
+
if (!line) {
|
|
3969
|
+
if (inList) {
|
|
3970
|
+
html += "</ul>";
|
|
3971
|
+
inList = false;
|
|
3972
|
+
}
|
|
3973
|
+
continue;
|
|
3974
|
+
}
|
|
3975
|
+
if (/^[\x00-\x1F\x7F-\x9F]+$/.test(line)) continue;
|
|
3976
|
+
if (line.includes("Normal.dot") || line.includes("Microsoft Word") || line.includes("Times New Roman") && line.length < 30) {
|
|
3977
|
+
continue;
|
|
3978
|
+
}
|
|
3979
|
+
if (line.length < 60 && !line.endsWith(".") && (/^[A-Z0-9\s:_-]+$/.test(line) || line.startsWith("#"))) {
|
|
3980
|
+
if (inList) {
|
|
3981
|
+
html += "</ul>";
|
|
3982
|
+
inList = false;
|
|
3983
|
+
}
|
|
3984
|
+
html += `<h2 style="font-size: 18px; font-weight: 700; color: #1e3a8a; margin: 20px 0 8px; border-bottom: 1px solid #e2e8f0; padding-bottom: 4px;">${DOMPurify5.sanitize(line)}</h2>`;
|
|
3985
|
+
} else if (line.startsWith("\u2022") || line.startsWith("-") || line.startsWith("*")) {
|
|
3986
|
+
if (!inList) {
|
|
3987
|
+
html += '<ul style="margin: 8px 0; padding-left: 24px;">';
|
|
3988
|
+
inList = true;
|
|
3989
|
+
}
|
|
3990
|
+
const bulletText = line.replace(/^[•\-\*]\s*/, "");
|
|
3991
|
+
html += `<li style="margin: 4px 0; line-height: 1.6;">${DOMPurify5.sanitize(bulletText)}</li>`;
|
|
3992
|
+
} else {
|
|
3993
|
+
if (inList) {
|
|
3994
|
+
html += "</ul>";
|
|
3995
|
+
inList = false;
|
|
3996
|
+
}
|
|
3997
|
+
html += `<p style="line-height: 1.7; margin: 10px 0; font-size: 14px; text-align: justify;">${DOMPurify5.sanitize(line)}</p>`;
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
if (inList) html += "</ul>";
|
|
4001
|
+
return html || `<p style="color: #64748b; font-style: italic;">(No readable text found in ${DOMPurify5.sanitize(filename)})</p>`;
|
|
4002
|
+
}
|
|
4003
|
+
};
|
|
4004
|
+
function docPlugin() {
|
|
4005
|
+
return new DocPlugin();
|
|
4006
|
+
}
|
|
4007
|
+
var PptPlugin = class {
|
|
4008
|
+
id = "ppt";
|
|
4009
|
+
name = "Legacy PowerPoint Presentation (.ppt, .pps, .pot)";
|
|
4010
|
+
extensions = [".ppt", ".pps", ".pot"];
|
|
4011
|
+
mimeTypes = ["application/vnd.ms-powerpoint"];
|
|
4012
|
+
weight = 75;
|
|
4013
|
+
supports(file) {
|
|
4014
|
+
const ext = file.metadata.extension?.toLowerCase();
|
|
4015
|
+
const mime = file.metadata.mimeType?.toLowerCase();
|
|
4016
|
+
return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
|
|
4017
|
+
}
|
|
4018
|
+
getToolbarActions(instance) {
|
|
4019
|
+
return [
|
|
4020
|
+
{
|
|
4021
|
+
id: "thumbnails",
|
|
4022
|
+
icon: "thumbnails",
|
|
4023
|
+
label: "Slide Thumbnails",
|
|
4024
|
+
type: "button",
|
|
4025
|
+
group: "navigation",
|
|
4026
|
+
execute: () => instance.toggleThumbnails?.()
|
|
4027
|
+
},
|
|
4028
|
+
{
|
|
4029
|
+
id: "page-prev",
|
|
4030
|
+
icon: "page-prev",
|
|
4031
|
+
label: "Previous Slide",
|
|
4032
|
+
type: "button",
|
|
4033
|
+
group: "navigation",
|
|
4034
|
+
execute: () => {
|
|
4035
|
+
const cur = instance.getCurrentPage?.() ?? 1;
|
|
4036
|
+
if (cur > 1) instance.goToPage?.(cur - 1);
|
|
4037
|
+
}
|
|
4038
|
+
},
|
|
4039
|
+
{
|
|
4040
|
+
id: "page-nav",
|
|
4041
|
+
icon: "",
|
|
4042
|
+
label: "Slide Number",
|
|
4043
|
+
type: "page-nav",
|
|
4044
|
+
group: "navigation",
|
|
4045
|
+
execute: (p) => instance.goToPage?.(Number(p))
|
|
4046
|
+
},
|
|
4047
|
+
{
|
|
4048
|
+
id: "page-next",
|
|
4049
|
+
icon: "page-next",
|
|
4050
|
+
label: "Next Slide",
|
|
4051
|
+
type: "button",
|
|
4052
|
+
group: "navigation",
|
|
4053
|
+
execute: () => {
|
|
4054
|
+
const cur = instance.getCurrentPage?.() ?? 1;
|
|
4055
|
+
const total = instance.getPageCount?.() ?? 1;
|
|
4056
|
+
if (cur < total) instance.goToPage?.(cur + 1);
|
|
4057
|
+
}
|
|
4058
|
+
},
|
|
4059
|
+
{
|
|
4060
|
+
id: "zoom-out",
|
|
4061
|
+
icon: "zoom-out",
|
|
4062
|
+
label: "Zoom Out",
|
|
4063
|
+
type: "button",
|
|
4064
|
+
group: "zoom",
|
|
4065
|
+
execute: () => instance.zoomOut?.()
|
|
4066
|
+
},
|
|
4067
|
+
{
|
|
4068
|
+
id: "zoom-in",
|
|
4069
|
+
icon: "zoom-in",
|
|
4070
|
+
label: "Zoom In",
|
|
4071
|
+
type: "button",
|
|
4072
|
+
group: "zoom",
|
|
4073
|
+
execute: () => instance.zoomIn?.()
|
|
4074
|
+
},
|
|
4075
|
+
{
|
|
4076
|
+
id: "fit-page",
|
|
4077
|
+
icon: "fit-page",
|
|
4078
|
+
label: "Fit to Slide",
|
|
4079
|
+
type: "button",
|
|
4080
|
+
group: "zoom",
|
|
4081
|
+
execute: () => instance.fitToPage?.()
|
|
4082
|
+
},
|
|
4083
|
+
{
|
|
4084
|
+
id: "download",
|
|
4085
|
+
icon: "download",
|
|
4086
|
+
label: "Download",
|
|
4087
|
+
type: "button",
|
|
4088
|
+
group: "actions",
|
|
4089
|
+
execute: () => instance.download?.()
|
|
4090
|
+
},
|
|
4091
|
+
{
|
|
4092
|
+
id: "print",
|
|
4093
|
+
icon: "print",
|
|
4094
|
+
label: "Print",
|
|
4095
|
+
type: "button",
|
|
4096
|
+
group: "actions",
|
|
4097
|
+
execute: () => instance.print?.()
|
|
4098
|
+
}
|
|
4099
|
+
];
|
|
4100
|
+
}
|
|
4101
|
+
async render(ctx) {
|
|
4102
|
+
const container = document.createElement("div");
|
|
4103
|
+
container.className = "fp-ppt-container";
|
|
4104
|
+
container.style.width = "100%";
|
|
4105
|
+
container.style.height = "100%";
|
|
4106
|
+
container.style.overflow = "auto";
|
|
4107
|
+
container.style.display = "flex";
|
|
4108
|
+
container.style.justifyContent = "center";
|
|
4109
|
+
container.style.alignItems = "center";
|
|
4110
|
+
container.style.padding = "32px 16px";
|
|
4111
|
+
container.style.backgroundColor = "#0f172a";
|
|
4112
|
+
const slideCard = document.createElement("div");
|
|
4113
|
+
slideCard.className = "fp-ppt-slide-card";
|
|
4114
|
+
slideCard.style.width = "960px";
|
|
4115
|
+
slideCard.style.maxWidth = "90%";
|
|
4116
|
+
slideCard.style.aspectRatio = "16 / 9";
|
|
4117
|
+
slideCard.style.backgroundColor = "#ffffff";
|
|
4118
|
+
slideCard.style.boxShadow = "0 8px 30px rgba(0,0,0,0.3)";
|
|
4119
|
+
slideCard.style.borderRadius = "8px";
|
|
4120
|
+
slideCard.style.padding = "48px";
|
|
4121
|
+
slideCard.style.display = "flex";
|
|
4122
|
+
slideCard.style.flexDirection = "column";
|
|
4123
|
+
slideCard.style.justifyContent = "center";
|
|
4124
|
+
slideCard.style.alignItems = "center";
|
|
4125
|
+
slideCard.style.boxSizing = "border-box";
|
|
4126
|
+
slideCard.style.position = "relative";
|
|
4127
|
+
slideCard.style.overflow = "hidden";
|
|
4128
|
+
slideCard.style.transformOrigin = "center center";
|
|
4129
|
+
slideCard.style.transition = "transform 0.2s ease";
|
|
4130
|
+
container.appendChild(slideCard);
|
|
4131
|
+
ctx.container.appendChild(container);
|
|
4132
|
+
let scale = 1;
|
|
4133
|
+
let currentSlide = 1;
|
|
4134
|
+
let slides = [];
|
|
4135
|
+
try {
|
|
4136
|
+
const cfbf = new CfbfReader(ctx.buffer);
|
|
4137
|
+
const pptStream = cfbf.readStream("PowerPoint Document");
|
|
4138
|
+
if (!pptStream || pptStream.length < 512) {
|
|
4139
|
+
throw new Error("PowerPoint Document stream not found in CFBF container");
|
|
4140
|
+
}
|
|
4141
|
+
slides = this.extractSlides(pptStream);
|
|
4142
|
+
} catch (err) {
|
|
4143
|
+
console.warn("[PptPlugin] Error extracting binary slides:", err);
|
|
4144
|
+
}
|
|
4145
|
+
if (slides.length === 0) {
|
|
4146
|
+
slides = [
|
|
4147
|
+
{
|
|
4148
|
+
title: ctx.metadata.name || "PowerPoint Presentation",
|
|
4149
|
+
texts: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"]
|
|
4150
|
+
}
|
|
4151
|
+
];
|
|
4152
|
+
}
|
|
4153
|
+
const totalSlides = slides.length;
|
|
4154
|
+
const renderSlide = (idx) => {
|
|
4155
|
+
currentSlide = idx;
|
|
4156
|
+
const s = slides[idx - 1];
|
|
4157
|
+
if (!s) return;
|
|
4158
|
+
slideCard.innerHTML = `
|
|
4159
|
+
<div style="position: absolute; top: 20px; right: 24px; font-size: 12px; color: #94a3b8; font-weight: 600;">
|
|
4160
|
+
Slide ${idx} of ${totalSlides}
|
|
4161
|
+
</div>
|
|
4162
|
+
<div style="text-align: center; width: 100%;">
|
|
4163
|
+
<h1 style="font-size: ${idx === 1 ? "36px" : "28px"}; color: #1e3a8a; margin: 0 0 24px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-weight: 700;">
|
|
4164
|
+
${DOMPurify5.sanitize(s.title || `Slide ${idx}`)}
|
|
4165
|
+
</h1>
|
|
4166
|
+
<div style="display: flex; flex-direction: column; gap: 12px; max-width: 80%; margin: 0 auto; text-align: ${idx === 1 ? "center" : "left"};">
|
|
4167
|
+
${s.texts.map((t) => `<div style="font-size: 18px; color: #334155; line-height: 1.5; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${DOMPurify5.sanitize(t)}</div>`).join("")}
|
|
4168
|
+
</div>
|
|
4169
|
+
</div>
|
|
4170
|
+
`;
|
|
4171
|
+
ctx.emit("page-change", { page: currentSlide, total: totalSlides });
|
|
4172
|
+
};
|
|
4173
|
+
renderSlide(1);
|
|
4174
|
+
const cleanup = () => {
|
|
4175
|
+
container.remove();
|
|
4176
|
+
ctx.container.innerHTML = "";
|
|
4177
|
+
};
|
|
4178
|
+
ctx.signal.addEventListener("abort", cleanup);
|
|
4179
|
+
return {
|
|
4180
|
+
destroy: cleanup,
|
|
4181
|
+
zoomIn: () => {
|
|
4182
|
+
scale += 0.1;
|
|
4183
|
+
slideCard.style.transform = `scale(${scale})`;
|
|
4184
|
+
},
|
|
4185
|
+
zoomOut: () => {
|
|
4186
|
+
scale = Math.max(0.3, scale - 0.1);
|
|
4187
|
+
slideCard.style.transform = `scale(${scale})`;
|
|
4188
|
+
},
|
|
4189
|
+
getZoom: () => scale,
|
|
4190
|
+
setZoom: (level) => {
|
|
4191
|
+
scale = level;
|
|
4192
|
+
slideCard.style.transform = `scale(${scale})`;
|
|
4193
|
+
},
|
|
4194
|
+
fitToPage: () => {
|
|
4195
|
+
scale = 1;
|
|
4196
|
+
slideCard.style.transform = "scale(1)";
|
|
4197
|
+
},
|
|
4198
|
+
goToPage: (page) => {
|
|
4199
|
+
if (page >= 1 && page <= totalSlides) {
|
|
4200
|
+
renderSlide(page);
|
|
4201
|
+
}
|
|
4202
|
+
},
|
|
4203
|
+
getPageCount: () => totalSlides,
|
|
4204
|
+
getCurrentPage: () => currentSlide,
|
|
4205
|
+
getThumbnails: async () => {
|
|
4206
|
+
return slides.map((s, idx) => ({
|
|
4207
|
+
index: idx,
|
|
4208
|
+
label: `Slide ${idx + 1}`,
|
|
4209
|
+
render: async (canvas) => {
|
|
4210
|
+
const ctx2d = canvas.getContext("2d");
|
|
4211
|
+
if (!ctx2d) return;
|
|
4212
|
+
canvas.width = 160;
|
|
4213
|
+
canvas.height = 90;
|
|
4214
|
+
ctx2d.fillStyle = "#ffffff";
|
|
4215
|
+
ctx2d.fillRect(0, 0, 160, 90);
|
|
4216
|
+
ctx2d.fillStyle = "#1e3a8a";
|
|
4217
|
+
ctx2d.font = "bold 11px sans-serif";
|
|
4218
|
+
ctx2d.textAlign = "center";
|
|
4219
|
+
const title = s.title.slice(0, 18) || `Slide ${idx + 1}`;
|
|
4220
|
+
ctx2d.fillText(title, 80, 50);
|
|
4221
|
+
}
|
|
4222
|
+
}));
|
|
4223
|
+
},
|
|
4224
|
+
download: () => {
|
|
4225
|
+
const blob = new Blob([ctx.buffer], { type: "application/vnd.ms-powerpoint" });
|
|
4226
|
+
const url = URL.createObjectURL(blob);
|
|
4227
|
+
const a = document.createElement("a");
|
|
4228
|
+
a.href = url;
|
|
4229
|
+
a.download = ctx.metadata.name || "presentation.ppt";
|
|
4230
|
+
a.click();
|
|
4231
|
+
URL.revokeObjectURL(url);
|
|
4232
|
+
},
|
|
4233
|
+
print: () => {
|
|
4234
|
+
window.print();
|
|
4235
|
+
}
|
|
4236
|
+
};
|
|
4237
|
+
}
|
|
4238
|
+
/**
|
|
4239
|
+
* Traverse PowerPoint binary stream records ([MS-PPT]) and extract text chunks per slide
|
|
4240
|
+
*/
|
|
4241
|
+
extractSlides(stream) {
|
|
4242
|
+
const view = new DataView(stream.buffer, stream.byteOffset, stream.byteLength);
|
|
4243
|
+
const len = stream.length;
|
|
4244
|
+
let offset = 0;
|
|
4245
|
+
const slides = [];
|
|
4246
|
+
let currentSlideTexts = [];
|
|
4247
|
+
while (offset + 8 <= len) {
|
|
4248
|
+
const recVerInst = view.getUint16(offset, true);
|
|
4249
|
+
const recType = view.getUint16(offset + 2, true);
|
|
4250
|
+
const recLen = view.getUint32(offset + 4, true);
|
|
4251
|
+
if (recType === 1006) {
|
|
4252
|
+
if (currentSlideTexts.length > 0) {
|
|
4253
|
+
const title = currentSlideTexts[0] || "Slide";
|
|
4254
|
+
const texts = currentSlideTexts.slice(1);
|
|
4255
|
+
slides.push({ title, texts });
|
|
4256
|
+
currentSlideTexts = [];
|
|
4257
|
+
}
|
|
4258
|
+
offset += 8;
|
|
4259
|
+
continue;
|
|
4260
|
+
}
|
|
4261
|
+
if (recType === 3998 && recLen > 0 && offset + 8 + recLen <= len) {
|
|
4262
|
+
const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
|
|
4263
|
+
const text = new TextDecoder("latin1").decode(bytes).trim();
|
|
4264
|
+
if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
|
|
4265
|
+
currentSlideTexts.push(text);
|
|
4266
|
+
}
|
|
4267
|
+
}
|
|
4268
|
+
if (recType === 3999 && recLen > 0 && offset + 8 + recLen <= len) {
|
|
4269
|
+
const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
|
|
4270
|
+
const text = new TextDecoder("utf-16le").decode(bytes).trim();
|
|
4271
|
+
if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
|
|
4272
|
+
currentSlideTexts.push(text);
|
|
4273
|
+
}
|
|
4274
|
+
}
|
|
4275
|
+
const isContainer = (recVerInst & 15) === 15;
|
|
4276
|
+
if (isContainer) {
|
|
4277
|
+
offset += 8;
|
|
4278
|
+
} else {
|
|
4279
|
+
offset += 8 + recLen;
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
if (currentSlideTexts.length > 0) {
|
|
4283
|
+
const title = currentSlideTexts[0] || "Slide";
|
|
4284
|
+
const texts = currentSlideTexts.slice(1);
|
|
4285
|
+
slides.push({ title, texts });
|
|
4286
|
+
}
|
|
4287
|
+
if (slides.length === 0) {
|
|
4288
|
+
const rawText = new TextDecoder("latin1", { fatal: false }).decode(stream);
|
|
4289
|
+
const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{4,}/g) || [];
|
|
4290
|
+
const filtered = matches.map((m) => m.trim()).filter((m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times"));
|
|
4291
|
+
if (filtered.length > 0) {
|
|
4292
|
+
const chunkSize = 4;
|
|
4293
|
+
for (let i = 0; i < filtered.length; i += chunkSize) {
|
|
4294
|
+
const chunk = filtered.slice(i, i + chunkSize);
|
|
4295
|
+
slides.push({
|
|
4296
|
+
title: chunk[0] || `Slide ${Math.floor(i / chunkSize) + 1}`,
|
|
4297
|
+
texts: chunk.slice(1)
|
|
4298
|
+
});
|
|
4299
|
+
}
|
|
4300
|
+
}
|
|
4301
|
+
}
|
|
4302
|
+
return slides;
|
|
4303
|
+
}
|
|
4304
|
+
};
|
|
4305
|
+
function pptPlugin() {
|
|
4306
|
+
return new PptPlugin();
|
|
4307
|
+
}
|
|
2737
4308
|
|
|
2738
4309
|
// src/index.ts
|
|
2739
4310
|
function getDefaultPlugins() {
|
|
@@ -2743,6 +4314,11 @@ function getDefaultPlugins() {
|
|
|
2743
4314
|
docxPlugin(),
|
|
2744
4315
|
excelPlugin(),
|
|
2745
4316
|
pptxPlugin(),
|
|
4317
|
+
docPlugin(),
|
|
4318
|
+
pptPlugin(),
|
|
4319
|
+
openDocumentPlugin(),
|
|
4320
|
+
rtfPlugin(),
|
|
4321
|
+
htmlPreviewPlugin(),
|
|
2746
4322
|
csvPlugin(),
|
|
2747
4323
|
archivePlugin(),
|
|
2748
4324
|
markdownPlugin(),
|
|
@@ -2759,6 +4335,6 @@ var FilePreviewViewer2 = class extends FilePreviewViewer {
|
|
|
2759
4335
|
}
|
|
2760
4336
|
};
|
|
2761
4337
|
|
|
2762
|
-
export { EventEmitter, FilePreviewViewer2 as FilePreviewViewer, ThumbnailPanel, ToolbarController, archivePlugin, clamp, codePlugin, createElement, csvPlugin, debounce, detectMagicBytes, detectOoxmlType, docxPlugin, downloadFile, excelPlugin, extractExtension, formatFileSize, getDefaultPlugins, markdownPlugin, mediaPlugin, mimeFromExtension, pdfPlugin, pptxPlugin, printElement, sanitizeHTML, sanitizeSVG, sourceToArrayBuffer, threeDPlugin };
|
|
4338
|
+
export { CfbfReader, EventEmitter, FilePreviewViewer2 as FilePreviewViewer, ThumbnailPanel, ToolbarController, archivePlugin, clamp, codePlugin, createElement, csvPlugin, debounce, detectMagicBytes, detectOoxmlType, docPlugin, docxPlugin, downloadFile, excelPlugin, extractExtension, formatFileSize, getDefaultPlugins, htmlPreviewPlugin, markdownPlugin, mediaPlugin, mimeFromExtension, openDocumentPlugin, pdfPlugin, pptPlugin, pptxPlugin, printElement, rtfPlugin, sanitizeHTML, sanitizeSVG, sourceToArrayBuffer, threeDPlugin };
|
|
2763
4339
|
//# sourceMappingURL=index.js.map
|
|
2764
4340
|
//# sourceMappingURL=index.js.map
|