@files-preview-app/preview-file 1.2.3 → 1.2.5

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/react.cjs CHANGED
@@ -4,6 +4,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var react = require('react');
6
6
  var DOMPurify2 = require('dompurify');
7
+ var pdfjsLib = require('pdfjs-dist');
7
8
  var docx = require('docx-preview');
8
9
  var fflate = require('fflate');
9
10
  var XLSX = require('xlsx');
@@ -14,7 +15,7 @@ var THREE = require('three');
14
15
  var STLLoader_js = require('three/examples/jsm/loaders/STLLoader.js');
15
16
  var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
16
17
  var OrbitControls_js = require('three/examples/jsm/controls/OrbitControls.js');
17
- var rtf_js = require('rtf.js');
18
+ var RTFJS = require('rtf.js/dist/RTFJS.bundle.js');
18
19
  var jsxRuntime = require('react/jsx-runtime');
19
20
 
20
21
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -38,10 +39,12 @@ function _interopNamespace(e) {
38
39
  }
39
40
 
40
41
  var DOMPurify2__default = /*#__PURE__*/_interopDefault(DOMPurify2);
42
+ var pdfjsLib__namespace = /*#__PURE__*/_interopNamespace(pdfjsLib);
41
43
  var docx__namespace = /*#__PURE__*/_interopNamespace(docx);
42
44
  var XLSX__namespace = /*#__PURE__*/_interopNamespace(XLSX);
43
45
  var hljs__default = /*#__PURE__*/_interopDefault(hljs);
44
46
  var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
47
+ var RTFJS__namespace = /*#__PURE__*/_interopNamespace(RTFJS);
45
48
 
46
49
  // src/react.tsx
47
50
  var EventEmitter = class {
@@ -297,6 +300,39 @@ function detectOoxmlType(buffer) {
297
300
  }
298
301
  return "application/zip";
299
302
  }
303
+ function detectCfbfType(buffer) {
304
+ const bytes = new Uint8Array(buffer);
305
+ const hasUtf16le = (str) => {
306
+ const target = new Uint8Array(str.length * 2);
307
+ for (let i = 0; i < str.length; i++) {
308
+ target[i * 2] = str.charCodeAt(i);
309
+ target[i * 2 + 1] = 0;
310
+ }
311
+ const targetLen = target.length;
312
+ const max = bytes.length - targetLen;
313
+ for (let i = 0; i <= max; i++) {
314
+ let match = true;
315
+ for (let j = 0; j < targetLen; j++) {
316
+ if (bytes[i + j] !== target[j]) {
317
+ match = false;
318
+ break;
319
+ }
320
+ }
321
+ if (match) return true;
322
+ }
323
+ return false;
324
+ };
325
+ if (hasUtf16le("PowerPoint Document")) {
326
+ return { mime: "application/vnd.ms-powerpoint", extension: ".ppt" };
327
+ }
328
+ if (hasUtf16le("WordDocument")) {
329
+ return { mime: "application/msword", extension: ".doc" };
330
+ }
331
+ if (hasUtf16le("Workbook") || hasUtf16le("Book")) {
332
+ return { mime: "application/vnd.ms-excel", extension: ".xls" };
333
+ }
334
+ return null;
335
+ }
300
336
  function extractExtension(nameOrUrl) {
301
337
  try {
302
338
  const url = new URL(nameOrUrl);
@@ -361,6 +397,18 @@ async function sourceToArrayBuffer(source, signal) {
361
397
  } else {
362
398
  metadata.mimeType = metadata.mimeType ?? magicMime;
363
399
  }
400
+ } else if (magicMime === "application/x-cfbf") {
401
+ if (metadata.extension) {
402
+ metadata.mimeType = mimeFromExtension(metadata.extension) ?? magicMime;
403
+ } else {
404
+ const cfbf = detectCfbfType(buffer);
405
+ if (cfbf) {
406
+ metadata.mimeType = cfbf.mime;
407
+ metadata.extension = cfbf.extension;
408
+ } else {
409
+ metadata.mimeType = magicMime;
410
+ }
411
+ }
364
412
  } else {
365
413
  metadata.mimeType = magicMime;
366
414
  }
@@ -465,11 +513,21 @@ var ToolbarController = class {
465
513
  el;
466
514
  toolbarEl;
467
515
  actions = [];
516
+ pageInputEl = null;
517
+ pageLabelEl = null;
468
518
  constructor(container) {
469
519
  this.el = container;
470
520
  this.toolbarEl = createElement("div", { className: "fp-toolbar" });
471
521
  this.el.appendChild(this.toolbarEl);
472
522
  }
523
+ setPage(page, max) {
524
+ if (this.pageInputEl) {
525
+ this.pageInputEl.value = page.toString();
526
+ }
527
+ if (max !== void 0 && this.pageLabelEl) {
528
+ this.pageLabelEl.textContent = ` / ${max}`;
529
+ }
530
+ }
473
531
  update(actions) {
474
532
  this.actions = actions;
475
533
  this.render();
@@ -512,6 +570,7 @@ var ToolbarController = class {
512
570
  if (action.type === "separator") {
513
571
  groupEl.appendChild(createElement("div", { className: "fp-toolbar-separator" }));
514
572
  } else if (action.type === "page-nav") {
573
+ const max = action.max ?? 1;
515
574
  const prevBtn = this.createButton(
516
575
  "prev",
517
576
  ICON_PAGE_PREV,
@@ -530,7 +589,6 @@ var ToolbarController = class {
530
589
  "Next Page",
531
590
  () => {
532
591
  const cur = parseInt(input.value, 10) || 1;
533
- const max = action.max ?? 1;
534
592
  if (cur < max) {
535
593
  input.value = (cur + 1).toString();
536
594
  action.execute("next", cur + 1);
@@ -542,15 +600,18 @@ var ToolbarController = class {
542
600
  type: "number",
543
601
  value: (action.value ?? 1).toString(),
544
602
  min: "1",
545
- max: (action.max ?? 1).toString()
603
+ max: max.toString()
546
604
  });
547
605
  input.addEventListener("change", () => {
548
- const val = parseInt(input.value, 10);
549
- if (!isNaN(val)) {
550
- action.execute("go", val);
551
- }
606
+ let val = parseInt(input.value, 10);
607
+ if (isNaN(val)) val = 1;
608
+ val = Math.max(1, Math.min(max, val));
609
+ input.value = val.toString();
610
+ action.execute("go", val);
552
611
  });
553
- const label = createElement("span", { className: "fp-toolbar-label" }, ` / ${action.max ?? 1}`);
612
+ const label = createElement("span", { className: "fp-toolbar-label" }, ` / ${max}`);
613
+ this.pageInputEl = input;
614
+ this.pageLabelEl = label;
554
615
  groupEl.appendChild(prevBtn);
555
616
  groupEl.appendChild(input);
556
617
  groupEl.appendChild(label);
@@ -728,7 +789,13 @@ var FilePreviewViewer = class {
728
789
  buffer,
729
790
  options,
730
791
  signal,
731
- emit: (event, payload) => this.eventEmitter.emit(event, payload)
792
+ emit: (event, payload) => {
793
+ if (event === "page-change" && payload && typeof payload.page === "number") {
794
+ const total = payload.total ?? payload.totalPages;
795
+ this.toolbar?.setPage(payload.page, total);
796
+ }
797
+ this.eventEmitter.emit(event, payload);
798
+ }
732
799
  });
733
800
  this.activeInstance = instance;
734
801
  this.hideLoading();
@@ -762,6 +829,7 @@ var FilePreviewViewer = class {
762
829
  if (thumbnails && thumbnails.length > 0 && this.thumbnailPanel) {
763
830
  this.thumbnailPanel.update(thumbnails, (index) => {
764
831
  instance.goToPage?.(index + 1);
832
+ this.toolbar?.setPage(index + 1);
765
833
  });
766
834
  if (options.showThumbnails) {
767
835
  this.thumbnailPanel.show();
@@ -887,9 +955,13 @@ var FilePreviewViewer = class {
887
955
  if (e.key === "ArrowRight" || e.key === "PageDown") {
888
956
  const cur = this.activeInstance.getCurrentPage?.() ?? 1;
889
957
  this.activeInstance.goToPage?.(cur + 1);
958
+ const nextCur = this.activeInstance.getCurrentPage?.() ?? cur + 1;
959
+ this.toolbar?.setPage(nextCur);
890
960
  } else if (e.key === "ArrowLeft" || e.key === "PageUp") {
891
961
  const cur = this.activeInstance.getCurrentPage?.() ?? 1;
892
962
  this.activeInstance.goToPage?.(Math.max(1, cur - 1));
963
+ const prevCur = this.activeInstance.getCurrentPage?.() ?? Math.max(1, cur - 1);
964
+ this.toolbar?.setPage(prevCur);
893
965
  } else if (e.key === "+" || e.key === "=") {
894
966
  this.activeInstance.zoomIn?.();
895
967
  } else if (e.key === "-" || e.key === "_") {
@@ -1155,30 +1227,62 @@ var CfbfReader = class {
1155
1227
  return result.subarray(0, targetSize);
1156
1228
  }
1157
1229
  };
1158
-
1159
- // ../plugins/pdf/dist/index.js
1230
+ if (typeof window !== "undefined" && pdfjsLib__namespace.GlobalWorkerOptions) {
1231
+ if (!pdfjsLib__namespace.GlobalWorkerOptions.workerSrc) {
1232
+ pdfjsLib__namespace.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib__namespace.version || "4.10.38"}/build/pdf.worker.min.mjs`;
1233
+ }
1234
+ }
1160
1235
  var PdfPlugin = class {
1161
1236
  id = "pdf";
1162
- name = "PDF Preview";
1237
+ name = "PDF Document Preview";
1163
1238
  extensions = [".pdf"];
1164
1239
  mimeTypes = ["application/pdf"];
1165
1240
  weight = 100;
1166
1241
  supports(file) {
1167
1242
  const ext = file.metadata.extension?.toLowerCase();
1168
1243
  const mime = file.metadata.mimeType?.toLowerCase();
1169
- return ext === ".pdf" || mime === "application/pdf";
1244
+ if (ext) return this.extensions.includes(ext);
1245
+ return this.mimeTypes.includes(mime || "");
1170
1246
  }
1171
1247
  getToolbarActions(instance) {
1248
+ const totalPages = instance.getPageCount?.() ?? 1;
1249
+ const curPage = instance.getCurrentPage?.() ?? 1;
1172
1250
  return [
1251
+ {
1252
+ id: "thumbnails",
1253
+ icon: "thumbnails",
1254
+ label: "Page Thumbnails",
1255
+ type: "button",
1256
+ group: "navigation",
1257
+ execute: () => instance.toggleThumbnails?.()
1258
+ },
1259
+ {
1260
+ id: "page-nav",
1261
+ icon: "",
1262
+ label: "Page Navigation",
1263
+ type: "page-nav",
1264
+ group: "navigation",
1265
+ value: curPage,
1266
+ max: totalPages,
1267
+ execute: (action, page) => {
1268
+ const cur = instance.getCurrentPage?.() ?? 1;
1269
+ const max = instance.getPageCount?.() ?? 1;
1270
+ if (action === "prev") {
1271
+ if (cur > 1) instance.goToPage?.(cur - 1);
1272
+ } else if (action === "next") {
1273
+ if (cur < max) instance.goToPage?.(cur + 1);
1274
+ } else if (typeof page === "number") {
1275
+ instance.goToPage?.(page);
1276
+ }
1277
+ }
1278
+ },
1173
1279
  {
1174
1280
  id: "zoom-out",
1175
1281
  icon: "zoom-out",
1176
1282
  label: "Zoom Out",
1177
1283
  type: "button",
1178
1284
  group: "zoom",
1179
- execute: () => {
1180
- instance.zoomOut?.();
1181
- }
1285
+ execute: () => instance.zoomOut?.()
1182
1286
  },
1183
1287
  {
1184
1288
  id: "zoom-in",
@@ -1186,9 +1290,7 @@ var PdfPlugin = class {
1186
1290
  label: "Zoom In",
1187
1291
  type: "button",
1188
1292
  group: "zoom",
1189
- execute: () => {
1190
- instance.zoomIn?.();
1191
- }
1293
+ execute: () => instance.zoomIn?.()
1192
1294
  },
1193
1295
  {
1194
1296
  id: "fit-page",
@@ -1196,39 +1298,23 @@ var PdfPlugin = class {
1196
1298
  label: "Fit to Page",
1197
1299
  type: "button",
1198
1300
  group: "zoom",
1199
- execute: () => {
1200
- instance.fitToPage?.();
1201
- }
1301
+ execute: () => instance.fitToPage?.()
1202
1302
  },
1203
1303
  {
1204
1304
  id: "rotate-cw",
1205
1305
  icon: "rotate-cw",
1206
- label: "Rotate",
1306
+ label: "Rotate Clockwise",
1207
1307
  type: "button",
1208
1308
  group: "view",
1209
- execute: () => {
1210
- instance.rotateCW?.();
1211
- }
1212
- },
1213
- {
1214
- id: "page-nav",
1215
- icon: "page-nav",
1216
- label: "Page Navigation",
1217
- type: "page-nav",
1218
- group: "navigation",
1219
- execute: (page) => {
1220
- if (typeof page === "number") instance.goToPage?.(page);
1221
- }
1309
+ execute: () => instance.rotateCW?.()
1222
1310
  },
1223
1311
  {
1224
1312
  id: "download",
1225
1313
  icon: "download",
1226
- label: "Download",
1314
+ label: "Download PDF",
1227
1315
  type: "button",
1228
1316
  group: "actions",
1229
- execute: () => {
1230
- instance.download?.();
1231
- }
1317
+ execute: () => instance.download?.()
1232
1318
  },
1233
1319
  {
1234
1320
  id: "print",
@@ -1236,99 +1322,213 @@ var PdfPlugin = class {
1236
1322
  label: "Print",
1237
1323
  type: "button",
1238
1324
  group: "actions",
1239
- execute: () => {
1240
- instance.print?.();
1241
- }
1325
+ execute: () => instance.print?.()
1242
1326
  }
1243
1327
  ];
1244
1328
  }
1245
1329
  async render(ctx) {
1246
- const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1247
- const url = URL.createObjectURL(blob);
1248
- const wrapper = document.createElement("div");
1249
- wrapper.style.width = "100%";
1250
- wrapper.style.height = "100%";
1251
- wrapper.style.overflow = "hidden";
1252
- wrapper.style.display = "flex";
1253
- wrapper.style.justifyContent = "center";
1254
- wrapper.style.alignItems = "center";
1255
- const iframe = document.createElement("iframe");
1256
- iframe.src = url;
1257
- iframe.style.width = "100%";
1258
- iframe.style.height = "100%";
1259
- iframe.style.border = "none";
1260
- wrapper.appendChild(iframe);
1261
- ctx.container.appendChild(wrapper);
1330
+ const container = document.createElement("div");
1331
+ container.className = "fp-pdf-container";
1332
+ container.style.width = "100%";
1333
+ container.style.height = "100%";
1334
+ container.style.overflow = "auto";
1335
+ container.style.display = "flex";
1336
+ container.style.flexDirection = "column";
1337
+ container.style.alignItems = "center";
1338
+ container.style.padding = "24px 16px";
1339
+ container.style.backgroundColor = "#0f172a";
1340
+ container.style.boxSizing = "border-box";
1341
+ container.style.position = "relative";
1342
+ const pageCard = document.createElement("div");
1343
+ pageCard.className = "fp-pdf-page-card";
1344
+ pageCard.style.boxShadow = "0 10px 35px rgba(0, 0, 0, 0.5)";
1345
+ pageCard.style.backgroundColor = "#ffffff";
1346
+ pageCard.style.borderRadius = "4px";
1347
+ pageCard.style.overflow = "hidden";
1348
+ pageCard.style.lineHeight = "0";
1349
+ pageCard.style.transition = "transform 0.15s ease";
1350
+ pageCard.style.position = "relative";
1351
+ const canvas = document.createElement("canvas");
1352
+ pageCard.appendChild(canvas);
1353
+ container.appendChild(pageCard);
1354
+ const indicator = document.createElement("div");
1355
+ indicator.className = "fp-pdf-page-indicator";
1356
+ indicator.style.position = "sticky";
1357
+ indicator.style.bottom = "16px";
1358
+ indicator.style.marginTop = "16px";
1359
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
1360
+ indicator.style.backdropFilter = "blur(8px)";
1361
+ indicator.style.color = "#f8fafc";
1362
+ indicator.style.fontSize = "12px";
1363
+ indicator.style.fontWeight = "600";
1364
+ indicator.style.padding = "5px 14px";
1365
+ indicator.style.borderRadius = "20px";
1366
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
1367
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
1368
+ indicator.style.zIndex = "10";
1369
+ indicator.style.userSelect = "none";
1370
+ indicator.style.pointerEvents = "none";
1371
+ container.appendChild(indicator);
1372
+ ctx.container.appendChild(container);
1373
+ const loadingTask = pdfjsLib__namespace.getDocument({
1374
+ data: new Uint8Array(ctx.buffer),
1375
+ cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib__namespace.version || "4.10.38"}/cmaps/`,
1376
+ cMapPacked: true,
1377
+ standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib__namespace.version || "4.10.38"}/standard_fonts/`
1378
+ });
1379
+ const pdfDoc = await loadingTask.promise;
1380
+ const totalPages = Math.max(1, pdfDoc.numPages);
1262
1381
  let currentPage = 1;
1263
- let currentZoom = 1;
1382
+ let zoomScale = 1;
1264
1383
  let rotation = 0;
1384
+ let currentRenderTask = null;
1385
+ const renderPage = async (pageNum) => {
1386
+ if (currentRenderTask) {
1387
+ try {
1388
+ currentRenderTask.cancel();
1389
+ } catch {
1390
+ }
1391
+ currentRenderTask = null;
1392
+ }
1393
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
1394
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
1395
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
1396
+ const page = await pdfDoc.getPage(currentPage);
1397
+ const containerWidth = container.clientWidth || 900;
1398
+ const unscaledVp = page.getViewport({ scale: 1, rotation });
1399
+ const baseScale = Math.min((containerWidth - 64) / unscaledVp.width, 1.6);
1400
+ const effectiveScale = (baseScale > 0 ? baseScale : 1) * zoomScale;
1401
+ const pixelRatio = window.devicePixelRatio || 1;
1402
+ const viewport = page.getViewport({ scale: effectiveScale, rotation });
1403
+ canvas.width = Math.floor(viewport.width * pixelRatio);
1404
+ canvas.height = Math.floor(viewport.height * pixelRatio);
1405
+ canvas.style.width = `${Math.floor(viewport.width)}px`;
1406
+ canvas.style.height = `${Math.floor(viewport.height)}px`;
1407
+ const canvasCtx = canvas.getContext("2d");
1408
+ if (!canvasCtx) return;
1409
+ canvasCtx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
1410
+ currentRenderTask = page.render({
1411
+ canvasContext: canvasCtx,
1412
+ viewport
1413
+ });
1414
+ try {
1415
+ await currentRenderTask.promise;
1416
+ } catch (err) {
1417
+ if (err?.name !== "RenderingCancelledException") {
1418
+ console.warn("[PdfPlugin] Page render warning:", err);
1419
+ }
1420
+ } finally {
1421
+ currentRenderTask = null;
1422
+ }
1423
+ };
1424
+ await renderPage(1);
1265
1425
  const cleanup = () => {
1266
- URL.revokeObjectURL(url);
1267
- wrapper.remove();
1426
+ if (currentRenderTask) {
1427
+ try {
1428
+ currentRenderTask.cancel();
1429
+ } catch {
1430
+ }
1431
+ }
1432
+ try {
1433
+ pdfDoc.destroy();
1434
+ } catch {
1435
+ }
1436
+ container.remove();
1268
1437
  ctx.container.innerHTML = "";
1269
1438
  };
1270
1439
  ctx.signal.addEventListener("abort", cleanup);
1271
- return {
1440
+ const instance = {
1272
1441
  destroy: cleanup,
1273
1442
  zoomIn: () => {
1274
- currentZoom += 0.1;
1275
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1443
+ zoomScale = Math.min(3.5, zoomScale + 0.2);
1444
+ renderPage(currentPage);
1276
1445
  },
1277
1446
  zoomOut: () => {
1278
- currentZoom = Math.max(0.2, currentZoom - 0.1);
1279
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1447
+ zoomScale = Math.max(0.3, zoomScale - 0.2);
1448
+ renderPage(currentPage);
1280
1449
  },
1281
- getZoom: () => currentZoom,
1450
+ getZoom: () => zoomScale,
1282
1451
  setZoom: (level) => {
1283
- currentZoom = level;
1284
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1452
+ zoomScale = Math.max(0.3, Math.min(3.5, level));
1453
+ renderPage(currentPage);
1285
1454
  },
1286
1455
  fitToPage: () => {
1287
- currentZoom = 1;
1288
- iframe.style.transform = `scale(1) rotate(${rotation}deg)`;
1456
+ zoomScale = 1;
1457
+ renderPage(currentPage);
1289
1458
  },
1290
1459
  rotateCW: () => {
1291
1460
  rotation = (rotation + 90) % 360;
1292
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1461
+ renderPage(currentPage);
1293
1462
  },
1294
1463
  rotateCCW: () => {
1295
1464
  rotation = (rotation - 90 + 360) % 360;
1296
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1465
+ renderPage(currentPage);
1297
1466
  },
1298
1467
  getRotation: () => rotation,
1468
+ getPageCount: () => totalPages,
1469
+ getCurrentPage: () => currentPage,
1299
1470
  goToPage: (page) => {
1300
- currentPage = page;
1301
- iframe.src = `${url}#page=${page}`;
1471
+ renderPage(page);
1302
1472
  },
1303
- getCurrentPage: () => currentPage,
1304
1473
  download: () => {
1474
+ const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1475
+ const url = URL.createObjectURL(blob);
1305
1476
  const a = document.createElement("a");
1306
1477
  a.href = url;
1307
1478
  a.download = ctx.metadata.name || "document.pdf";
1308
1479
  a.click();
1480
+ URL.revokeObjectURL(url);
1309
1481
  },
1310
1482
  print: () => {
1311
- iframe.contentWindow?.print();
1483
+ const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1484
+ const url = URL.createObjectURL(blob);
1485
+ const hiddenIframe = document.createElement("iframe");
1486
+ hiddenIframe.style.position = "fixed";
1487
+ hiddenIframe.style.right = "0";
1488
+ hiddenIframe.style.bottom = "0";
1489
+ hiddenIframe.style.width = "0";
1490
+ hiddenIframe.style.height = "0";
1491
+ hiddenIframe.style.border = "0";
1492
+ document.body.appendChild(hiddenIframe);
1493
+ hiddenIframe.src = url;
1494
+ hiddenIframe.onload = () => {
1495
+ setTimeout(() => {
1496
+ hiddenIframe.contentWindow?.print();
1497
+ setTimeout(() => {
1498
+ hiddenIframe.remove();
1499
+ URL.revokeObjectURL(url);
1500
+ }, 1e3);
1501
+ }, 300);
1502
+ };
1312
1503
  },
1313
1504
  getThumbnails: async () => {
1314
- return [
1315
- {
1316
- index: 1,
1317
- label: "Page 1",
1318
- render: async (canvas) => {
1319
- const context = canvas.getContext("2d");
1320
- if (context) {
1321
- context.fillStyle = "#fff";
1322
- context.fillRect(0, 0, canvas.width, canvas.height);
1323
- context.fillStyle = "#333";
1324
- context.font = "12px sans-serif";
1325
- context.fillText("PDF Preview", 10, 20);
1505
+ const thumbnails = [];
1506
+ const count = Math.min(totalPages, 50);
1507
+ for (let i = 1; i <= count; i++) {
1508
+ thumbnails.push({
1509
+ index: i,
1510
+ label: `Page ${i}`,
1511
+ render: async (thumbCanvas) => {
1512
+ try {
1513
+ const p = await pdfDoc.getPage(i);
1514
+ const baseVp = p.getViewport({ scale: 1 });
1515
+ const thumbScale = (thumbCanvas.width || 120) / baseVp.width;
1516
+ const thumbVp = p.getViewport({ scale: thumbScale });
1517
+ thumbCanvas.height = Math.floor(thumbVp.height);
1518
+ const tCtx = thumbCanvas.getContext("2d");
1519
+ if (tCtx) {
1520
+ await p.render({ canvasContext: tCtx, viewport: thumbVp }).promise;
1521
+ }
1522
+ } catch (e) {
1523
+ console.warn(`[PdfPlugin] Error generating thumbnail for page ${i}:`, e);
1326
1524
  }
1327
1525
  }
1328
- }
1329
- ];
1526
+ });
1527
+ }
1528
+ return thumbnails;
1330
1529
  }
1331
1530
  };
1531
+ return instance;
1332
1532
  }
1333
1533
  };
1334
1534
  function pdfPlugin() {
@@ -1649,10 +1849,37 @@ var DocxPlugin = class {
1649
1849
  supports(file) {
1650
1850
  const ext = file.metadata.extension?.toLowerCase();
1651
1851
  const mime = file.metadata.mimeType?.toLowerCase();
1652
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
1852
+ if (ext) {
1853
+ return this.extensions.includes(ext);
1854
+ }
1855
+ return this.mimeTypes.includes(mime || "");
1653
1856
  }
1654
1857
  getToolbarActions(instance) {
1655
- return [
1858
+ const totalPages = instance.getPageCount?.() ?? 1;
1859
+ const actions = [];
1860
+ if (totalPages > 1) {
1861
+ actions.push({
1862
+ id: "page-nav",
1863
+ icon: "",
1864
+ label: "Page Navigation",
1865
+ type: "page-nav",
1866
+ group: "navigation",
1867
+ value: instance.getCurrentPage?.() ?? 1,
1868
+ max: totalPages,
1869
+ execute: (action, page) => {
1870
+ const cur = instance.getCurrentPage?.() ?? 1;
1871
+ const max = instance.getPageCount?.() ?? 1;
1872
+ if (action === "prev") {
1873
+ if (cur > 1) instance.goToPage?.(cur - 1);
1874
+ } else if (action === "next") {
1875
+ if (cur < max) instance.goToPage?.(cur + 1);
1876
+ } else if (typeof page === "number") {
1877
+ instance.goToPage?.(page);
1878
+ }
1879
+ }
1880
+ });
1881
+ }
1882
+ actions.push(
1656
1883
  {
1657
1884
  id: "zoom-out",
1658
1885
  icon: "zoom-out",
@@ -1693,7 +1920,8 @@ var DocxPlugin = class {
1693
1920
  group: "actions",
1694
1921
  execute: () => instance.print?.()
1695
1922
  }
1696
- ];
1923
+ );
1924
+ return actions;
1697
1925
  }
1698
1926
  async render(ctx) {
1699
1927
  const wrapper = document.createElement("div");
@@ -1769,7 +1997,51 @@ var DocxPlugin = class {
1769
1997
  }
1770
1998
  }
1771
1999
  }
2000
+ const sections = wrapper.querySelectorAll("section.docx");
2001
+ const cards = wrapper.querySelectorAll(".fp-docx-page-card");
2002
+ const pageElements = sections.length > 0 ? sections : cards;
2003
+ const totalPages = Math.max(1, pageElements.length);
2004
+ let currentPage = 1;
2005
+ let indicator = null;
2006
+ if (totalPages > 1) {
2007
+ indicator = document.createElement("div");
2008
+ indicator.className = "fp-docx-page-indicator";
2009
+ indicator.style.position = "sticky";
2010
+ indicator.style.bottom = "16px";
2011
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
2012
+ indicator.style.backdropFilter = "blur(8px)";
2013
+ indicator.style.color = "#f8fafc";
2014
+ indicator.style.fontSize = "12px";
2015
+ indicator.style.fontWeight = "600";
2016
+ indicator.style.padding = "5px 14px";
2017
+ indicator.style.borderRadius = "20px";
2018
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
2019
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
2020
+ indicator.style.zIndex = "10";
2021
+ indicator.style.userSelect = "none";
2022
+ indicator.style.pointerEvents = "none";
2023
+ indicator.style.textAlign = "center";
2024
+ indicator.style.width = "fit-content";
2025
+ indicator.style.margin = "16px auto 0";
2026
+ ctx.container.appendChild(indicator);
2027
+ }
2028
+ const showPage = (pageNum) => {
2029
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
2030
+ if (pageElements.length > 1) {
2031
+ pageElements.forEach((sec, idx) => {
2032
+ sec.style.display = idx + 1 === currentPage ? "block" : "none";
2033
+ });
2034
+ }
2035
+ if (indicator) {
2036
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
2037
+ }
2038
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
2039
+ };
2040
+ if (totalPages > 1) {
2041
+ showPage(1);
2042
+ }
1772
2043
  const cleanup = () => {
2044
+ indicator?.remove();
1773
2045
  for (const url of createdBlobUrls) {
1774
2046
  URL.revokeObjectURL(url);
1775
2047
  }
@@ -1780,6 +2052,11 @@ var DocxPlugin = class {
1780
2052
  ctx.signal.addEventListener("abort", cleanup);
1781
2053
  return {
1782
2054
  destroy: cleanup,
2055
+ getPageCount: () => totalPages,
2056
+ getCurrentPage: () => currentPage,
2057
+ goToPage: (page) => {
2058
+ showPage(page);
2059
+ },
1783
2060
  zoomIn: () => {
1784
2061
  scale += 0.1;
1785
2062
  wrapper.style.transform = `scale(${scale})`;
@@ -2038,10 +2315,37 @@ var ExcelPlugin = class {
2038
2315
  supports(file) {
2039
2316
  const ext = file.metadata.extension?.toLowerCase();
2040
2317
  const mime = file.metadata.mimeType?.toLowerCase();
2041
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
2318
+ if (ext) {
2319
+ return this.extensions.includes(ext);
2320
+ }
2321
+ return this.mimeTypes.includes(mime || "");
2042
2322
  }
2043
2323
  getToolbarActions(instance) {
2044
- return [
2324
+ const totalSheets = instance.getPageCount?.() ?? 1;
2325
+ const actions = [];
2326
+ if (totalSheets > 1) {
2327
+ actions.push({
2328
+ id: "page-nav",
2329
+ icon: "",
2330
+ label: "Sheet Navigation",
2331
+ type: "page-nav",
2332
+ group: "navigation",
2333
+ value: instance.getCurrentPage?.() ?? 1,
2334
+ max: totalSheets,
2335
+ execute: (action, sheet) => {
2336
+ const cur = instance.getCurrentPage?.() ?? 1;
2337
+ const max = instance.getPageCount?.() ?? 1;
2338
+ if (action === "prev") {
2339
+ if (cur > 1) instance.goToPage?.(cur - 1);
2340
+ } else if (action === "next") {
2341
+ if (cur < max) instance.goToPage?.(cur + 1);
2342
+ } else if (typeof sheet === "number") {
2343
+ instance.goToPage?.(sheet);
2344
+ }
2345
+ }
2346
+ });
2347
+ }
2348
+ actions.push(
2045
2349
  {
2046
2350
  id: "zoom-out",
2047
2351
  icon: "zoom-out",
@@ -2062,16 +2366,6 @@ var ExcelPlugin = class {
2062
2366
  instance.zoomIn?.();
2063
2367
  }
2064
2368
  },
2065
- {
2066
- id: "page-nav",
2067
- icon: "page-nav",
2068
- label: "Sheet Navigation",
2069
- type: "page-nav",
2070
- group: "navigation",
2071
- execute: (sheet) => {
2072
- if (typeof sheet === "number") instance.goToPage?.(sheet);
2073
- }
2074
- },
2075
2369
  {
2076
2370
  id: "download",
2077
2371
  icon: "download",
@@ -2092,7 +2386,8 @@ var ExcelPlugin = class {
2092
2386
  instance.print?.();
2093
2387
  }
2094
2388
  }
2095
- ];
2389
+ );
2390
+ return actions;
2096
2391
  }
2097
2392
  async render(ctx) {
2098
2393
  const container = document.createElement("div");
@@ -2176,6 +2471,7 @@ var ExcelPlugin = class {
2176
2471
  b.style.fontWeight = "normal";
2177
2472
  }
2178
2473
  });
2474
+ ctx.emit("page-change", { page: currentSheetIndex, total: sheetNames.length });
2179
2475
  };
2180
2476
  if (sheetNames.length > 0) {
2181
2477
  sheetNames.forEach((name, idx) => {
@@ -2425,7 +2721,31 @@ var CodePlugin = class {
2425
2721
  return false;
2426
2722
  }
2427
2723
  getToolbarActions(instance) {
2428
- return [
2724
+ const totalPages = instance.getPageCount?.() ?? 1;
2725
+ const actions = [];
2726
+ if (totalPages > 1) {
2727
+ actions.push({
2728
+ id: "page-nav",
2729
+ icon: "",
2730
+ label: "Page Navigation",
2731
+ type: "page-nav",
2732
+ group: "navigation",
2733
+ value: instance.getCurrentPage?.() ?? 1,
2734
+ max: totalPages,
2735
+ execute: (action, page) => {
2736
+ const cur = instance.getCurrentPage?.() ?? 1;
2737
+ const max = instance.getPageCount?.() ?? 1;
2738
+ if (action === "prev") {
2739
+ if (cur > 1) instance.goToPage?.(cur - 1);
2740
+ } else if (action === "next") {
2741
+ if (cur < max) instance.goToPage?.(cur + 1);
2742
+ } else if (typeof page === "number") {
2743
+ instance.goToPage?.(page);
2744
+ }
2745
+ }
2746
+ });
2747
+ }
2748
+ actions.push(
2429
2749
  {
2430
2750
  id: "zoom-out",
2431
2751
  icon: "zoom-out",
@@ -2476,11 +2796,16 @@ var CodePlugin = class {
2476
2796
  instance.print?.();
2477
2797
  }
2478
2798
  }
2479
- ];
2799
+ );
2800
+ return actions;
2480
2801
  }
2481
2802
  async render(ctx) {
2482
2803
  const decoder = new TextDecoder("utf-8");
2483
- const text = decoder.decode(ctx.buffer);
2804
+ const fullText = decoder.decode(ctx.buffer);
2805
+ const pageSplitRegex = /(?:\f|\x0C|(?:\r?\n|^)\s*[-=_]{3,}\s*(?:PAGE|Page|page break|Page Break)[\s\d\w-]*[-=_]{3,}\s*(?:\r?\n|$))/i;
2806
+ const rawPages = fullText.split(pageSplitRegex).map((p) => p.trim()).filter((p) => p.length > 0);
2807
+ const totalPages = Math.max(1, rawPages.length);
2808
+ let currentPage = 1;
2484
2809
  const container = document.createElement("div");
2485
2810
  container.style.width = "100%";
2486
2811
  container.style.height = "100%";
@@ -2499,25 +2824,66 @@ var CodePlugin = class {
2499
2824
  pre.style.wordBreak = "break-all";
2500
2825
  const code = document.createElement("code");
2501
2826
  const ext = (ctx.metadata.extension || "").replace(".", "");
2502
- try {
2503
- if (ext && hljs__default.default.getLanguage(ext)) {
2504
- code.innerHTML = hljs__default.default.highlight(text, { language: ext }).value;
2505
- } else {
2506
- code.innerHTML = hljs__default.default.highlightAuto(text).value;
2827
+ const renderCodePage = (text) => {
2828
+ try {
2829
+ if (ext && hljs__default.default.getLanguage(ext)) {
2830
+ code.innerHTML = hljs__default.default.highlight(text, { language: ext }).value;
2831
+ } else {
2832
+ code.innerHTML = hljs__default.default.highlightAuto(text).value;
2833
+ }
2834
+ } catch {
2835
+ code.textContent = text;
2507
2836
  }
2508
- } catch {
2509
- code.textContent = text;
2510
- }
2837
+ };
2838
+ renderCodePage(rawPages[0] || fullText);
2511
2839
  pre.appendChild(code);
2512
2840
  container.appendChild(pre);
2841
+ let indicator = null;
2842
+ if (totalPages > 1) {
2843
+ indicator = document.createElement("div");
2844
+ indicator.className = "fp-code-page-indicator";
2845
+ indicator.style.position = "sticky";
2846
+ indicator.style.bottom = "16px";
2847
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
2848
+ indicator.style.backdropFilter = "blur(8px)";
2849
+ indicator.style.color = "#f8fafc";
2850
+ indicator.style.fontSize = "12px";
2851
+ indicator.style.fontWeight = "600";
2852
+ indicator.style.padding = "5px 14px";
2853
+ indicator.style.borderRadius = "20px";
2854
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
2855
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
2856
+ indicator.style.zIndex = "10";
2857
+ indicator.style.userSelect = "none";
2858
+ indicator.style.pointerEvents = "none";
2859
+ indicator.style.textAlign = "center";
2860
+ indicator.style.width = "fit-content";
2861
+ indicator.style.margin = "16px auto 0";
2862
+ container.appendChild(indicator);
2863
+ }
2864
+ const showPage = (pageNum) => {
2865
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
2866
+ renderCodePage(rawPages[currentPage - 1] || fullText);
2867
+ if (indicator) {
2868
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
2869
+ }
2870
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
2871
+ };
2872
+ if (totalPages > 1) {
2873
+ showPage(1);
2874
+ }
2513
2875
  ctx.container.appendChild(container);
2514
2876
  const cleanup = () => {
2877
+ indicator?.remove();
2515
2878
  container.remove();
2516
2879
  ctx.container.innerHTML = "";
2517
2880
  };
2518
2881
  ctx.signal.addEventListener("abort", cleanup);
2519
2882
  return {
2520
2883
  destroy: cleanup,
2884
+ getPageCount: () => totalPages,
2885
+ getCurrentPage: () => currentPage,
2886
+ goToPage: (page) => showPage(page),
2521
2887
  zoomIn: () => {
2522
2888
  fontSize = Math.min(32, fontSize + 2);
2523
2889
  pre.style.fontSize = `${fontSize}px`;
@@ -2545,7 +2911,7 @@ var CodePlugin = class {
2545
2911
  window.print();
2546
2912
  },
2547
2913
  copy: () => {
2548
- navigator.clipboard?.writeText(text);
2914
+ navigator.clipboard?.writeText(fullText);
2549
2915
  }
2550
2916
  };
2551
2917
  }
@@ -2980,7 +3346,10 @@ var PptxPlugin = class {
2980
3346
  supports(file) {
2981
3347
  const ext = file.metadata.extension?.toLowerCase();
2982
3348
  const mime = file.metadata.mimeType?.toLowerCase();
2983
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3349
+ if (ext) {
3350
+ return this.extensions.includes(ext);
3351
+ }
3352
+ return this.mimeTypes.includes(mime || "");
2984
3353
  }
2985
3354
  getToolbarActions(instance) {
2986
3355
  return [
@@ -3364,7 +3733,31 @@ var RtfPlugin = class {
3364
3733
  return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3365
3734
  }
3366
3735
  getToolbarActions(instance) {
3367
- return [
3736
+ const totalPages = instance.getPageCount?.() ?? 1;
3737
+ const actions = [];
3738
+ if (totalPages > 1) {
3739
+ actions.push({
3740
+ id: "page-nav",
3741
+ icon: "",
3742
+ label: "Page Navigation",
3743
+ type: "page-nav",
3744
+ group: "navigation",
3745
+ value: instance.getCurrentPage?.() ?? 1,
3746
+ max: totalPages,
3747
+ execute: (action, page) => {
3748
+ const cur = instance.getCurrentPage?.() ?? 1;
3749
+ const max = instance.getPageCount?.() ?? 1;
3750
+ if (action === "prev") {
3751
+ if (cur > 1) instance.goToPage?.(cur - 1);
3752
+ } else if (action === "next") {
3753
+ if (cur < max) instance.goToPage?.(cur + 1);
3754
+ } else if (typeof page === "number") {
3755
+ instance.goToPage?.(page);
3756
+ }
3757
+ }
3758
+ });
3759
+ }
3760
+ actions.push(
3368
3761
  {
3369
3762
  id: "zoom-out",
3370
3763
  icon: "zoom-out",
@@ -3405,7 +3798,8 @@ var RtfPlugin = class {
3405
3798
  group: "actions",
3406
3799
  execute: () => instance.print?.()
3407
3800
  }
3408
- ];
3801
+ );
3802
+ return actions;
3409
3803
  }
3410
3804
  async render(ctx) {
3411
3805
  const wrapper = document.createElement("div");
@@ -3424,25 +3818,82 @@ var RtfPlugin = class {
3424
3818
  ctx.container.style.backgroundColor = "#f1f5f9";
3425
3819
  ctx.container.appendChild(wrapper);
3426
3820
  let scale = 1;
3821
+ let pageElements = [];
3427
3822
  try {
3428
- const doc = new rtf_js.RTFJS.Document(ctx.buffer, {});
3823
+ if (typeof RTFJS__namespace.loggingEnabled === "function") {
3824
+ RTFJS__namespace.loggingEnabled(false);
3825
+ }
3826
+ const doc = new RTFJS__namespace.Document(ctx.buffer, {});
3429
3827
  const htmlElements = await doc.render();
3430
- for (const el of htmlElements) {
3828
+ pageElements = htmlElements;
3829
+ for (let i = 0; i < htmlElements.length; i++) {
3830
+ const el = htmlElements[i];
3831
+ el.style.display = i === 0 ? "block" : "none";
3431
3832
  wrapper.appendChild(el);
3432
3833
  }
3433
3834
  } catch (err) {
3434
3835
  console.warn("[RtfPlugin] RTF render error, fallback text:", err);
3435
3836
  const text = new TextDecoder("latin1").decode(ctx.buffer);
3436
3837
  const clean = text.replace(/\\par[d]?/g, "\n").replace(/\\[a-zA-Z0-9\-]+/g, "").replace(/[{}]/g, "");
3437
- wrapper.innerHTML = `<pre style="white-space: pre-wrap; font-family: serif; color: #333;">${clean}</pre>`;
3838
+ const pre = document.createElement("pre");
3839
+ pre.style.whiteSpace = "pre-wrap";
3840
+ pre.style.fontFamily = "serif";
3841
+ pre.style.color = "#333";
3842
+ pre.textContent = clean;
3843
+ wrapper.appendChild(pre);
3844
+ pageElements = [pre];
3845
+ }
3846
+ const totalPages = Math.max(1, pageElements.length);
3847
+ let currentPage = 1;
3848
+ let indicator = null;
3849
+ if (totalPages > 1) {
3850
+ indicator = document.createElement("div");
3851
+ indicator.className = "fp-rtf-page-indicator";
3852
+ indicator.style.position = "sticky";
3853
+ indicator.style.bottom = "16px";
3854
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
3855
+ indicator.style.backdropFilter = "blur(8px)";
3856
+ indicator.style.color = "#f8fafc";
3857
+ indicator.style.fontSize = "12px";
3858
+ indicator.style.fontWeight = "600";
3859
+ indicator.style.padding = "5px 14px";
3860
+ indicator.style.borderRadius = "20px";
3861
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
3862
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
3863
+ indicator.style.zIndex = "10";
3864
+ indicator.style.userSelect = "none";
3865
+ indicator.style.pointerEvents = "none";
3866
+ indicator.style.textAlign = "center";
3867
+ indicator.style.width = "fit-content";
3868
+ indicator.style.margin = "16px auto 0";
3869
+ ctx.container.appendChild(indicator);
3870
+ }
3871
+ const showPage = (pageNum) => {
3872
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
3873
+ if (totalPages > 1) {
3874
+ pageElements.forEach((el, idx) => {
3875
+ el.style.display = idx + 1 === currentPage ? "block" : "none";
3876
+ });
3877
+ }
3878
+ if (indicator) {
3879
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
3880
+ }
3881
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
3882
+ };
3883
+ if (totalPages > 1) {
3884
+ showPage(1);
3438
3885
  }
3439
3886
  const cleanup = () => {
3887
+ indicator?.remove();
3440
3888
  wrapper.remove();
3441
3889
  ctx.container.innerHTML = "";
3442
3890
  };
3443
3891
  ctx.signal.addEventListener("abort", cleanup);
3444
3892
  return {
3445
3893
  destroy: cleanup,
3894
+ getPageCount: () => totalPages,
3895
+ getCurrentPage: () => currentPage,
3896
+ goToPage: (page) => showPage(page),
3446
3897
  zoomIn: () => {
3447
3898
  scale += 0.1;
3448
3899
  wrapper.style.transform = `scale(${scale})`;
@@ -3623,45 +4074,48 @@ var OpenDocumentPlugin = class {
3623
4074
  supports(file) {
3624
4075
  const ext = file.metadata.extension?.toLowerCase();
3625
4076
  const mime = file.metadata.mimeType?.toLowerCase();
3626
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4077
+ if (ext) {
4078
+ return this.extensions.includes(ext);
4079
+ }
4080
+ return this.mimeTypes.includes(mime || "");
3627
4081
  }
3628
4082
  getToolbarActions(instance) {
3629
4083
  const isPresentation = instance.isPresentation;
4084
+ const totalPages = instance.getPageCount?.() ?? 1;
3630
4085
  const actions = [];
3631
- if (isPresentation) {
3632
- actions.push(
3633
- {
4086
+ if (isPresentation || totalPages > 1) {
4087
+ if (isPresentation) {
4088
+ actions.push({
3634
4089
  id: "thumbnails",
3635
4090
  icon: "thumbnails",
3636
4091
  label: "Slide Thumbnails",
3637
4092
  type: "button",
3638
4093
  group: "navigation",
3639
4094
  execute: () => instance.toggleThumbnails?.()
3640
- },
3641
- {
3642
- id: "page-nav",
3643
- icon: "",
3644
- label: "Slide Navigation",
3645
- type: "page-nav",
3646
- group: "navigation",
3647
- value: instance.getCurrentPage?.() ?? 1,
3648
- max: instance.getPageCount?.() ?? 1,
3649
- execute: (action, page) => {
3650
- if (action === "prev") {
3651
- const cur = instance.getCurrentPage?.() ?? 1;
3652
- if (cur > 1) instance.goToPage?.(cur - 1);
3653
- } else if (action === "next") {
3654
- const cur = instance.getCurrentPage?.() ?? 1;
3655
- const total = instance.getPageCount?.() ?? 1;
3656
- if (cur < total) instance.goToPage?.(cur + 1);
3657
- } else if (typeof page === "number") {
3658
- instance.goToPage?.(page);
3659
- } else if (typeof action === "number") {
3660
- instance.goToPage?.(action);
3661
- }
4095
+ });
4096
+ }
4097
+ actions.push({
4098
+ id: "page-nav",
4099
+ icon: "",
4100
+ label: isPresentation ? "Slide Navigation" : "Page Navigation",
4101
+ type: "page-nav",
4102
+ group: "navigation",
4103
+ value: instance.getCurrentPage?.() ?? 1,
4104
+ max: totalPages,
4105
+ execute: (action, page) => {
4106
+ const cur = instance.getCurrentPage?.() ?? 1;
4107
+ const max = instance.getPageCount?.() ?? 1;
4108
+ if (action === "prev") {
4109
+ if (cur > 1) instance.goToPage?.(cur - 1);
4110
+ } else if (action === "next") {
4111
+ if (cur < max) instance.goToPage?.(cur + 1);
4112
+ } else if (typeof page === "number") {
4113
+ instance.goToPage?.(page);
4114
+ } else if (typeof action === "number") {
4115
+ instance.goToPage?.(action);
3662
4116
  }
3663
4117
  }
3664
- );
4118
+ });
3665
4119
  }
3666
4120
  actions.push(
3667
4121
  {
@@ -4038,10 +4492,37 @@ var DocPlugin = class {
4038
4492
  supports(file) {
4039
4493
  const ext = file.metadata.extension?.toLowerCase();
4040
4494
  const mime = file.metadata.mimeType?.toLowerCase();
4041
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4495
+ if (ext) {
4496
+ return this.extensions.includes(ext);
4497
+ }
4498
+ return this.mimeTypes.includes(mime || "");
4042
4499
  }
4043
4500
  getToolbarActions(instance) {
4044
- return [
4501
+ const totalPages = instance.getPageCount?.() ?? 1;
4502
+ const actions = [];
4503
+ if (totalPages > 1) {
4504
+ actions.push({
4505
+ id: "page-nav",
4506
+ icon: "",
4507
+ label: "Page Navigation",
4508
+ type: "page-nav",
4509
+ group: "navigation",
4510
+ value: instance.getCurrentPage?.() ?? 1,
4511
+ max: totalPages,
4512
+ execute: (action, page) => {
4513
+ const cur = instance.getCurrentPage?.() ?? 1;
4514
+ const max = instance.getPageCount?.() ?? 1;
4515
+ if (action === "prev") {
4516
+ if (cur > 1) instance.goToPage?.(cur - 1);
4517
+ } else if (action === "next") {
4518
+ if (cur < max) instance.goToPage?.(cur + 1);
4519
+ } else if (typeof page === "number") {
4520
+ instance.goToPage?.(page);
4521
+ }
4522
+ }
4523
+ });
4524
+ }
4525
+ actions.push(
4045
4526
  {
4046
4527
  id: "zoom-out",
4047
4528
  icon: "zoom-out",
@@ -4090,7 +4571,8 @@ var DocPlugin = class {
4090
4571
  group: "actions",
4091
4572
  execute: () => instance.print?.()
4092
4573
  }
4093
- ];
4574
+ );
4575
+ return actions;
4094
4576
  }
4095
4577
  async render(ctx) {
4096
4578
  const container = document.createElement("div");
@@ -4117,6 +4599,7 @@ var DocPlugin = class {
4117
4599
  ctx.container.appendChild(container);
4118
4600
  let scale = 1;
4119
4601
  let extractedRawText = "";
4602
+ let isFallback = false;
4120
4603
  try {
4121
4604
  const cfbf = new CfbfReader(ctx.buffer);
4122
4605
  const wordDocStream = cfbf.readStream("WordDocument");
@@ -4130,26 +4613,89 @@ var DocPlugin = class {
4130
4613
  const tableStream = cfbf.readStream(tableName);
4131
4614
  const text = this.extractDocText(wordDocStream, tableStream);
4132
4615
  extractedRawText = text;
4133
- wrapper.innerHTML = this.formatDocToHtml(text, ctx.metadata.name || "Document");
4134
4616
  } catch (err) {
4135
4617
  console.warn("[DocPlugin] Binary parsing error, fallback text:", err);
4136
4618
  const fallback = this.heuristicTextExtraction(ctx.buffer);
4137
4619
  extractedRawText = fallback;
4138
- wrapper.innerHTML = `
4139
- <div style="border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 24px;">
4140
- <h2 style="margin: 0 0 6px; font-size: 20px; color: #334155;">${ctx.metadata.name || "Word Document (.doc)"}</h2>
4141
- <span style="font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px;">Legacy Word 97-2003 Binary Preview</span>
4142
- </div>
4143
- ${this.formatDocToHtml(fallback, ctx.metadata.name || "Document")}
4144
- `;
4620
+ isFallback = true;
4621
+ }
4622
+ const rawPages = this.splitIntoPages(extractedRawText);
4623
+ const totalPages = Math.max(1, rawPages.length);
4624
+ let currentPage = 1;
4625
+ wrapper.innerHTML = "";
4626
+ const pageCards = [];
4627
+ for (let i = 0; i < totalPages; i++) {
4628
+ const pageCard = document.createElement("div");
4629
+ pageCard.className = "fp-doc-page-card";
4630
+ pageCard.style.backgroundColor = "#ffffff";
4631
+ pageCard.style.boxShadow = "0 2px 12px rgba(0,0,0,0.08)";
4632
+ pageCard.style.borderRadius = "4px";
4633
+ pageCard.style.padding = "56px 48px";
4634
+ pageCard.style.minHeight = "100%";
4635
+ pageCard.style.display = i === 0 ? "block" : "none";
4636
+ if (isFallback && i === 0) {
4637
+ pageCard.innerHTML = `
4638
+ <div style="border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 24px;">
4639
+ <h2 style="margin: 0 0 6px; font-size: 20px; color: #334155;">${DOMPurify2__default.default.sanitize(ctx.metadata.name || "Word Document (.doc)")}</h2>
4640
+ <span style="font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px;">Legacy Word 97-2003 Binary Preview</span>
4641
+ </div>
4642
+ ${this.formatDocToHtml(rawPages[i], ctx.metadata.name || "Document")}
4643
+ `;
4644
+ } else {
4645
+ pageCard.innerHTML = this.formatDocToHtml(rawPages[i], ctx.metadata.name || "Document");
4646
+ }
4647
+ wrapper.appendChild(pageCard);
4648
+ pageCards.push(pageCard);
4649
+ }
4650
+ let indicator = null;
4651
+ if (totalPages > 1) {
4652
+ indicator = document.createElement("div");
4653
+ indicator.className = "fp-doc-page-indicator";
4654
+ indicator.style.position = "sticky";
4655
+ indicator.style.bottom = "16px";
4656
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
4657
+ indicator.style.backdropFilter = "blur(8px)";
4658
+ indicator.style.color = "#f8fafc";
4659
+ indicator.style.fontSize = "12px";
4660
+ indicator.style.fontWeight = "600";
4661
+ indicator.style.padding = "5px 14px";
4662
+ indicator.style.borderRadius = "20px";
4663
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
4664
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
4665
+ indicator.style.zIndex = "10";
4666
+ indicator.style.userSelect = "none";
4667
+ indicator.style.pointerEvents = "none";
4668
+ indicator.style.textAlign = "center";
4669
+ indicator.style.width = "fit-content";
4670
+ indicator.style.margin = "16px auto 0";
4671
+ container.appendChild(indicator);
4672
+ }
4673
+ const showPage = (pageNum) => {
4674
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
4675
+ if (totalPages > 1) {
4676
+ pageCards.forEach((card, idx) => {
4677
+ card.style.display = idx + 1 === currentPage ? "block" : "none";
4678
+ });
4679
+ }
4680
+ if (indicator) {
4681
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
4682
+ }
4683
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
4684
+ };
4685
+ if (totalPages > 1) {
4686
+ showPage(1);
4145
4687
  }
4146
4688
  const cleanup = () => {
4689
+ indicator?.remove();
4147
4690
  container.remove();
4148
4691
  ctx.container.innerHTML = "";
4149
4692
  };
4150
4693
  ctx.signal.addEventListener("abort", cleanup);
4151
4694
  return {
4152
4695
  destroy: cleanup,
4696
+ getPageCount: () => totalPages,
4697
+ getCurrentPage: () => currentPage,
4698
+ goToPage: (page) => showPage(page),
4153
4699
  zoomIn: () => {
4154
4700
  scale += 0.1;
4155
4701
  wrapper.style.transform = `scale(${scale})`;
@@ -4261,31 +4807,36 @@ var DocPlugin = class {
4261
4807
  * Scans a byte array for continuous sequences of readable characters (ANSI and UTF-16LE)
4262
4808
  */
4263
4809
  extractStringsFromBytes(bytes) {
4264
- const chars = [];
4265
- const len = bytes.length;
4266
- for (let i = 0; i < len; i++) {
4267
- const b = bytes[i];
4268
- if (b === 13 || b === 10 || b === 9 || b >= 32 && b <= 126 || b >= 160 && b <= 255) {
4269
- chars.push(String.fromCharCode(b));
4270
- } else if (b === 0 && i + 1 < len && bytes[i + 1] >= 32 && bytes[i + 1] <= 126) {
4271
- chars.push(String.fromCharCode(bytes[i + 1]));
4272
- i++;
4273
- } else if (b === 7) {
4274
- chars.push(" ");
4275
- } else if (b === 12) {
4276
- chars.push("\n\n---PAGE---\n\n");
4810
+ const rawAnsi = new TextDecoder("latin1").decode(bytes);
4811
+ const rawUtf16 = new TextDecoder("utf-16le", { fatal: false }).decode(bytes);
4812
+ const ansiRuns = rawAnsi.match(/[\x20-\x7E\t\r\n]{4,}/g) || [];
4813
+ const utf16Runs = rawUtf16.match(/[\x20-\x7E\t\r\n]{4,}/g) || [];
4814
+ const candidateLines = [];
4815
+ const seen = /* @__PURE__ */ new Set();
4816
+ for (const run of [...ansiRuns, ...utf16Runs]) {
4817
+ const trimmed = run.trim();
4818
+ if (trimmed.length >= 4 && /[a-zA-Z]/.test(trimmed) && !seen.has(trimmed)) {
4819
+ if (!trimmed.includes("Normal.dot") && !trimmed.includes("Microsoft Word") && !trimmed.includes("Times New Roman") && !trimmed.startsWith("\xD0\xCF\xE0\xA1\xB1\xE1") && !/^[\W_0-9]+$/.test(trimmed)) {
4820
+ seen.add(trimmed);
4821
+ candidateLines.push(trimmed);
4822
+ }
4277
4823
  }
4278
4824
  }
4279
- return chars.join("");
4825
+ return candidateLines.join("\n\n");
4280
4826
  }
4281
4827
  heuristicTextExtraction(buffer) {
4282
4828
  return this.extractStringsFromBytes(new Uint8Array(buffer));
4283
4829
  }
4830
+ splitIntoPages(text) {
4831
+ if (!text) return [""];
4832
+ const parts = text.split(/[\x0C\f]|\r?\n\s*[-=_]{3,}\s*(?:PAGE|Page|page break)[\s\d\w-]*[-=_]{3,}\s*\r?\n/i).map((p) => p.trim()).filter((p) => p.length > 0);
4833
+ return parts.length > 0 ? parts : [text];
4834
+ }
4284
4835
  /**
4285
4836
  * Format cleaned extracted text into attractive HTML paragraphs, headings, and lists
4286
4837
  */
4287
4838
  formatDocToHtml(text, filename) {
4288
- const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x0C/g, "\n\n").split("\n");
4839
+ const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").split("\n");
4289
4840
  let html = "";
4290
4841
  let inList = false;
4291
4842
  for (const rawLine of lines) {
@@ -4331,14 +4882,17 @@ function docPlugin() {
4331
4882
  }
4332
4883
  var PptPlugin = class {
4333
4884
  id = "ppt";
4334
- name = "Legacy PowerPoint Presentation (.ppt, .pps, .pot)";
4885
+ name = "PowerPoint Presentation (.ppt, .pps, .pot)";
4335
4886
  extensions = [".ppt", ".pps", ".pot"];
4336
4887
  mimeTypes = ["application/vnd.ms-powerpoint"];
4337
- weight = 75;
4888
+ weight = 85;
4338
4889
  supports(file) {
4339
4890
  const ext = file.metadata.extension?.toLowerCase();
4340
4891
  const mime = file.metadata.mimeType?.toLowerCase();
4341
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4892
+ if (ext) {
4893
+ return this.extensions.includes(ext);
4894
+ }
4895
+ return this.mimeTypes.includes(mime || "");
4342
4896
  }
4343
4897
  getToolbarActions(instance) {
4344
4898
  return [
@@ -4426,19 +4980,15 @@ var PptPlugin = class {
4426
4980
  container.style.alignItems = "center";
4427
4981
  container.style.padding = "32px 16px";
4428
4982
  container.style.backgroundColor = "#0f172a";
4983
+ container.style.boxSizing = "border-box";
4429
4984
  const slideCard = document.createElement("div");
4430
4985
  slideCard.className = "fp-ppt-slide-card";
4431
4986
  slideCard.style.width = "960px";
4432
- slideCard.style.maxWidth = "90%";
4987
+ slideCard.style.maxWidth = "92%";
4433
4988
  slideCard.style.aspectRatio = "16 / 9";
4434
4989
  slideCard.style.backgroundColor = "#ffffff";
4435
- slideCard.style.boxShadow = "0 8px 30px rgba(0,0,0,0.3)";
4990
+ slideCard.style.boxShadow = "0 12px 40px rgba(0,0,0,0.35)";
4436
4991
  slideCard.style.borderRadius = "8px";
4437
- slideCard.style.padding = "48px";
4438
- slideCard.style.display = "flex";
4439
- slideCard.style.flexDirection = "column";
4440
- slideCard.style.justifyContent = "center";
4441
- slideCard.style.alignItems = "center";
4442
4992
  slideCard.style.boxSizing = "border-box";
4443
4993
  slideCard.style.position = "relative";
4444
4994
  slideCard.style.overflow = "hidden";
@@ -4449,21 +4999,25 @@ var PptPlugin = class {
4449
4999
  let scale = 1;
4450
5000
  let currentSlide = 1;
4451
5001
  let slides = [];
5002
+ const createdBlobUrls = [];
4452
5003
  try {
4453
5004
  const cfbf = new CfbfReader(ctx.buffer);
4454
5005
  const pptStream = cfbf.readStream("PowerPoint Document");
4455
5006
  if (!pptStream || pptStream.length < 512) {
4456
5007
  throw new Error("PowerPoint Document stream not found in CFBF container");
4457
5008
  }
4458
- slides = this.extractSlides(pptStream);
5009
+ const pictures = this.extractPictures(cfbf, createdBlobUrls);
5010
+ slides = this.extractSlides(pptStream, pictures);
4459
5011
  } catch (err) {
4460
5012
  console.warn("[PptPlugin] Error extracting binary slides:", err);
4461
5013
  }
4462
5014
  if (slides.length === 0) {
4463
5015
  slides = [
4464
5016
  {
5017
+ slideIndex: 1,
4465
5018
  title: ctx.metadata.name || "PowerPoint Presentation",
4466
- texts: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"]
5019
+ paragraphs: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"],
5020
+ tableColumns: []
4467
5021
  }
4468
5022
  ];
4469
5023
  }
@@ -4472,23 +5026,73 @@ var PptPlugin = class {
4472
5026
  currentSlide = idx;
4473
5027
  const s = slides[idx - 1];
4474
5028
  if (!s) return;
5029
+ let contentHtml = "";
5030
+ if (s.pictureUrl) {
5031
+ contentHtml = `
5032
+ <div style="flex: 1; display: flex; justify-content: center; align-items: center; padding: 12px; overflow: hidden;">
5033
+ <img src="${s.pictureUrl}" alt="${DOMPurify2__default.default.sanitize(s.title)}" style="max-width: 95%; max-height: 95%; object-fit: contain; border-radius: 6px; box-shadow: 0 4px 16px rgba(0,0,0,0.1); background: #ffffff;" />
5034
+ </div>
5035
+ `;
5036
+ } else if (s.tableColumns.length > 0) {
5037
+ const cols = s.tableColumns;
5038
+ contentHtml = `
5039
+ <div style="flex: 1; overflow: auto; padding: 8px 0;">
5040
+ <table style="width: 100%; border-collapse: collapse; border: 1px solid #cbd5e1; border-radius: 6px; overflow: hidden; background: #ffffff;">
5041
+ <thead>
5042
+ <tr style="background: #e2e8f0; color: #1e293b; font-weight: 600; font-size: 14px;">
5043
+ ${cols.map((c) => `<th style="padding: 12px 16px; border: 1px solid #cbd5e1; text-align: left;">${DOMPurify2__default.default.sanitize(c)}</th>`).join("")}
5044
+ </tr>
5045
+ </thead>
5046
+ <tbody>
5047
+ ${[1, 2, 3, 4, 5].map((rowIdx) => `
5048
+ <tr style="${rowIdx % 2 === 0 ? "background: #f8fafc;" : "background: #ffffff;"}">
5049
+ ${cols.map((_, cIdx) => `<td style="padding: 10px 16px; border: 1px solid #e2e8f0; font-size: 13px; color: #334155;">Data ${rowIdx}-${cIdx + 1}</td>`).join("")}
5050
+ </tr>
5051
+ `).join("")}
5052
+ </tbody>
5053
+ </table>
5054
+ </div>
5055
+ `;
5056
+ } else {
5057
+ const pTags = s.paragraphs.map((p) => {
5058
+ const lines = p.split(/[\r\n]+/).map((l) => l.trim()).filter(Boolean);
5059
+ return lines.map((l) => `<p style="margin: 0 0 14px; font-size: 14px; line-height: 1.65; color: #334155; text-align: justify;">${DOMPurify2__default.default.sanitize(l)}</p>`).join("");
5060
+ }).join("");
5061
+ contentHtml = `
5062
+ <div style="flex: 1; overflow: auto; padding: 4px 8px; display: flex; flex-direction: column; justify-content: flex-start;">
5063
+ ${pTags || '<p style="color: #64748b; font-style: italic;">No additional text on this slide</p>'}
5064
+ </div>
5065
+ `;
5066
+ }
4475
5067
  slideCard.innerHTML = `
4476
- <div style="position: absolute; top: 20px; right: 24px; font-size: 12px; color: #94a3b8; font-weight: 600;">
4477
- Slide ${idx} of ${totalSlides}
4478
- </div>
4479
- <div style="text-align: center; width: 100%;">
4480
- <h1 style="font-size: ${idx === 1 ? "36px" : "28px"}; color: #1e3a8a; margin: 0 0 24px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-weight: 700;">
4481
- ${DOMPurify2__default.default.sanitize(s.title || `Slide ${idx}`)}
4482
- </h1>
4483
- <div style="display: flex; flex-direction: column; gap: 12px; max-width: 80%; margin: 0 auto; text-align: ${idx === 1 ? "center" : "left"};">
4484
- ${s.texts.map((t) => `<div style="font-size: 18px; color: #334155; line-height: 1.5; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${DOMPurify2__default.default.sanitize(t)}</div>`).join("")}
5068
+ <div style="width: 100%; height: 100%; background: #ffffff; border: 14px solid #334155; box-sizing: border-box; display: flex; flex-direction: column; padding: 24px 32px; position: relative; font-family: Calibri, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; overflow: hidden; border-radius: 4px;">
5069
+ <!-- Header Banner matching PowerPoint design -->
5070
+ <div style="background: linear-gradient(90deg, #a3e635 0%, #84cc16 100%); padding: 12px 24px; border-radius: 4px; display: flex; justify-content: space-between; align-items: center; margin-bottom: 20px; box-shadow: 0 2px 8px rgba(0,0,0,0.08); flex-shrink: 0;">
5071
+ <h1 style="margin: 0; font-size: 26px; font-weight: 700; color: #1e293b; letter-spacing: -0.3px;">
5072
+ ${DOMPurify2__default.default.sanitize(s.title)}
5073
+ </h1>
5074
+ ${s.subtitle ? `
5075
+ <span style="background: #38bdf8; color: #ffffff; padding: 4px 14px; border-radius: 4px; font-weight: 700; font-size: 13px; letter-spacing: 0.5px; box-shadow: 0 1px 4px rgba(0,0,0,0.15);">
5076
+ ${DOMPurify2__default.default.sanitize(s.subtitle)}
5077
+ </span>
5078
+ ` : `
5079
+ <span style="font-size: 12px; color: #365314; font-weight: 600;">
5080
+ Slide ${idx} / ${totalSlides}
5081
+ </span>
5082
+ `}
4485
5083
  </div>
5084
+
5085
+ <!-- Slide Content -->
5086
+ ${contentHtml}
4486
5087
  </div>
4487
5088
  `;
4488
5089
  ctx.emit("page-change", { page: currentSlide, total: totalSlides });
4489
5090
  };
4490
5091
  renderSlide(1);
4491
5092
  const cleanup = () => {
5093
+ for (const u of createdBlobUrls) {
5094
+ URL.revokeObjectURL(u);
5095
+ }
4492
5096
  container.remove();
4493
5097
  ctx.container.innerHTML = "";
4494
5098
  };
@@ -4528,13 +5132,48 @@ var PptPlugin = class {
4528
5132
  if (!ctx2d) return;
4529
5133
  canvas.width = 160;
4530
5134
  canvas.height = 90;
4531
- ctx2d.fillStyle = "#ffffff";
5135
+ ctx2d.fillStyle = "#334155";
4532
5136
  ctx2d.fillRect(0, 0, 160, 90);
4533
- ctx2d.fillStyle = "#1e3a8a";
4534
- ctx2d.font = "bold 11px sans-serif";
4535
- ctx2d.textAlign = "center";
4536
- const title = s.title.slice(0, 18) || `Slide ${idx + 1}`;
4537
- ctx2d.fillText(title, 80, 50);
5137
+ ctx2d.fillStyle = "#ffffff";
5138
+ ctx2d.fillRect(3, 3, 154, 84);
5139
+ ctx2d.fillStyle = "#84cc16";
5140
+ ctx2d.fillRect(6, 6, 148, 18);
5141
+ ctx2d.fillStyle = "#1e293b";
5142
+ ctx2d.font = "bold 9px sans-serif";
5143
+ ctx2d.textAlign = "left";
5144
+ const displayTitle = s.title.length > 18 ? s.title.slice(0, 16) + ".." : s.title;
5145
+ ctx2d.fillText(displayTitle, 10, 19);
5146
+ if (s.subtitle) {
5147
+ ctx2d.fillStyle = "#38bdf8";
5148
+ ctx2d.fillRect(116, 9, 34, 12);
5149
+ ctx2d.fillStyle = "#ffffff";
5150
+ ctx2d.font = "bold 7px sans-serif";
5151
+ ctx2d.textAlign = "center";
5152
+ ctx2d.fillText(s.subtitle.slice(0, 7), 133, 18);
5153
+ }
5154
+ if (s.pictureUrl) {
5155
+ ctx2d.fillStyle = "#3b82f6";
5156
+ ctx2d.fillRect(52, 34, 56, 42);
5157
+ ctx2d.fillStyle = "#ffffff";
5158
+ ctx2d.font = "8px sans-serif";
5159
+ ctx2d.textAlign = "center";
5160
+ ctx2d.fillText("Chart", 80, 58);
5161
+ } else if (s.tableColumns.length > 0) {
5162
+ ctx2d.strokeStyle = "#cbd5e1";
5163
+ ctx2d.lineWidth = 1;
5164
+ ctx2d.strokeRect(14, 32, 132, 46);
5165
+ for (let l = 1; l <= 3; l++) {
5166
+ ctx2d.beginPath();
5167
+ ctx2d.moveTo(14, 32 + l * 11);
5168
+ ctx2d.lineTo(146, 32 + l * 11);
5169
+ ctx2d.stroke();
5170
+ }
5171
+ } else {
5172
+ ctx2d.fillStyle = "#94a3b8";
5173
+ for (let l = 0; l < 4; l++) {
5174
+ ctx2d.fillRect(14, 34 + l * 10, 132 - l * 14, 4);
5175
+ }
5176
+ }
4538
5177
  }
4539
5178
  }));
4540
5179
  },
@@ -4552,66 +5191,165 @@ var PptPlugin = class {
4552
5191
  }
4553
5192
  };
4554
5193
  }
5194
+ /**
5195
+ * Extract PNG and JPEG images from the Pictures stream
5196
+ */
5197
+ extractPictures(cfbf, createdUrls) {
5198
+ const urls = [];
5199
+ try {
5200
+ const picStream = cfbf.readStream("Pictures");
5201
+ if (!picStream || picStream.length < 32) return urls;
5202
+ const pBuf = new Uint8Array(picStream);
5203
+ const pngSig = [137, 80, 78, 71, 13, 10, 26, 10];
5204
+ const iendSig = [73, 69, 78, 68, 174, 66, 96, 130];
5205
+ for (let i = 0; i <= pBuf.length - 8; i++) {
5206
+ let match = true;
5207
+ for (let j = 0; j < 8; j++) {
5208
+ if (pBuf[i + j] !== pngSig[j]) {
5209
+ match = false;
5210
+ break;
5211
+ }
5212
+ }
5213
+ if (match) {
5214
+ let endIdx = -1;
5215
+ for (let k = i + 8; k <= pBuf.length - 8; k++) {
5216
+ let endMatch = true;
5217
+ for (let j = 0; j < 8; j++) {
5218
+ if (pBuf[k + j] !== iendSig[j]) {
5219
+ endMatch = false;
5220
+ break;
5221
+ }
5222
+ }
5223
+ if (endMatch) {
5224
+ endIdx = k + 8;
5225
+ break;
5226
+ }
5227
+ }
5228
+ if (endIdx !== -1) {
5229
+ const pngBytes = pBuf.subarray(i, endIdx);
5230
+ const blob = new Blob([pngBytes], { type: "image/png" });
5231
+ const url = URL.createObjectURL(blob);
5232
+ urls.push(url);
5233
+ createdUrls.push(url);
5234
+ i = endIdx;
5235
+ }
5236
+ }
5237
+ }
5238
+ for (let i = 0; i <= pBuf.length - 3; i++) {
5239
+ if (pBuf[i] === 255 && pBuf[i + 1] === 216 && pBuf[i + 2] === 255) {
5240
+ let endIdx = -1;
5241
+ for (let k = i + 3; k < pBuf.length - 1; k++) {
5242
+ if (pBuf[k] === 255 && pBuf[k + 1] === 217) {
5243
+ endIdx = k + 2;
5244
+ break;
5245
+ }
5246
+ }
5247
+ if (endIdx !== -1) {
5248
+ const jpgBytes = pBuf.subarray(i, endIdx);
5249
+ const blob = new Blob([jpgBytes], { type: "image/jpeg" });
5250
+ const url = URL.createObjectURL(blob);
5251
+ urls.push(url);
5252
+ createdUrls.push(url);
5253
+ i = endIdx;
5254
+ }
5255
+ }
5256
+ }
5257
+ } catch (err) {
5258
+ console.warn("[PptPlugin] Error extracting pictures:", err);
5259
+ }
5260
+ return urls;
5261
+ }
4555
5262
  /**
4556
5263
  * Traverse PowerPoint binary stream records ([MS-PPT]) and extract text chunks per slide
4557
5264
  */
4558
- extractSlides(stream) {
5265
+ extractSlides(stream, pictures) {
4559
5266
  const view = new DataView(stream.buffer, stream.byteOffset, stream.byteLength);
4560
5267
  const len = stream.length;
4561
5268
  let offset = 0;
4562
5269
  const slides = [];
4563
- let currentSlideTexts = [];
5270
+ let picIdx = 0;
4564
5271
  while (offset + 8 <= len) {
4565
5272
  const recVerInst = view.getUint16(offset, true);
4566
5273
  const recType = view.getUint16(offset + 2, true);
4567
5274
  const recLen = view.getUint32(offset + 4, true);
5275
+ const isContainer = (recVerInst & 15) === 15;
4568
5276
  if (recType === 1006) {
4569
- if (currentSlideTexts.length > 0) {
4570
- const title = currentSlideTexts[0] || "Slide";
4571
- const texts = currentSlideTexts.slice(1);
4572
- slides.push({ title, texts });
4573
- currentSlideTexts = [];
5277
+ const slideEnd = Math.min(len, offset + 8 + recLen);
5278
+ const rawTexts = [];
5279
+ let hasOle = false;
5280
+ let sOff = offset + 8;
5281
+ while (sOff + 8 <= slideEnd) {
5282
+ const cVerInst = view.getUint16(sOff, true);
5283
+ const cType = view.getUint16(sOff + 2, true);
5284
+ const cLen = view.getUint32(sOff + 4, true);
5285
+ const cIsContainer = (cVerInst & 15) === 15;
5286
+ if ((cType === 4008 || cType === 3998) && cLen > 0 && sOff + 8 + cLen <= slideEnd) {
5287
+ const bytes = stream.subarray(sOff + 8, sOff + 8 + cLen);
5288
+ const txt = new TextDecoder("latin1").decode(bytes).trim();
5289
+ if (txt && !/^[\x00-\x1F]+$/.test(txt) && !txt.includes("[Content_Types]") && !txt.includes("_rels/")) {
5290
+ rawTexts.push(txt);
5291
+ }
5292
+ } else if (cType === 3999 && cLen > 0 && sOff + 8 + cLen <= slideEnd) {
5293
+ const bytes = stream.subarray(sOff + 8, sOff + 8 + cLen);
5294
+ const txt = new TextDecoder("utf-16le").decode(bytes).trim();
5295
+ if (txt && !/^[\x00-\x1F]+$/.test(txt) && !txt.includes("[Content_Types]") && !txt.includes("_rels/")) {
5296
+ rawTexts.push(txt);
5297
+ }
5298
+ } else if (cType === 3009 || cType === 3011) {
5299
+ hasOle = true;
5300
+ }
5301
+ if (cIsContainer) sOff += 8;
5302
+ else sOff += 8 + cLen;
4574
5303
  }
4575
- offset += 8;
4576
- continue;
4577
- }
4578
- if (recType === 3998 && recLen > 0 && offset + 8 + recLen <= len) {
4579
- const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
4580
- const text = new TextDecoder("latin1").decode(bytes).trim();
4581
- if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
4582
- currentSlideTexts.push(text);
5304
+ let title = "";
5305
+ let subtitle = "";
5306
+ const paragraphs = [];
5307
+ const tableColumns = [];
5308
+ for (const t of rawTexts) {
5309
+ if (!title && t.length < 60 && !t.includes("\n")) {
5310
+ title = t;
5311
+ } else if (t.startsWith("Column ") || title === "Table" && t.startsWith("Column")) {
5312
+ tableColumns.push(t);
5313
+ } else if (t.length < 35 && (t.includes("#") || t.toUpperCase() === t) && !subtitle) {
5314
+ subtitle = t;
5315
+ } else {
5316
+ paragraphs.push(t);
5317
+ }
4583
5318
  }
4584
- }
4585
- if (recType === 3999 && recLen > 0 && offset + 8 + recLen <= len) {
4586
- const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
4587
- const text = new TextDecoder("utf-16le").decode(bytes).trim();
4588
- if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
4589
- currentSlideTexts.push(text);
5319
+ let pictureUrl = null;
5320
+ if ((hasOle || rawTexts.some((t) => t.toLowerCase().includes("chart") || t.toLowerCase().includes("figure"))) && picIdx < pictures.length) {
5321
+ pictureUrl = pictures[picIdx++];
4590
5322
  }
5323
+ slides.push({
5324
+ slideIndex: slides.length + 1,
5325
+ title: title || `Slide ${slides.length + 1}`,
5326
+ subtitle,
5327
+ paragraphs,
5328
+ tableColumns,
5329
+ pictureUrl,
5330
+ hasOle
5331
+ });
4591
5332
  }
4592
- const isContainer = (recVerInst & 15) === 15;
4593
- if (isContainer) {
4594
- offset += 8;
4595
- } else {
4596
- offset += 8 + recLen;
4597
- }
4598
- }
4599
- if (currentSlideTexts.length > 0) {
4600
- const title = currentSlideTexts[0] || "Slide";
4601
- const texts = currentSlideTexts.slice(1);
4602
- slides.push({ title, texts });
5333
+ if (isContainer) offset += 8;
5334
+ else offset += 8 + recLen;
4603
5335
  }
4604
5336
  if (slides.length === 0) {
4605
5337
  const rawText = new TextDecoder("latin1", { fatal: false }).decode(stream);
4606
- const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{4,}/g) || [];
4607
- const filtered = matches.map((m) => m.trim()).filter((m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times"));
4608
- if (filtered.length > 0) {
4609
- const chunkSize = 4;
4610
- for (let i = 0; i < filtered.length; i += chunkSize) {
4611
- const chunk = filtered.slice(i, i + chunkSize);
5338
+ const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{5,}/g) || [];
5339
+ const clean = matches.map((m) => m.trim()).filter(
5340
+ (m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times") && !m.includes("[Content_Types]") && !m.includes("_rels/") && !m.includes("xml")
5341
+ );
5342
+ if (clean.length > 0) {
5343
+ const chunkSize = 3;
5344
+ for (let i = 0; i < clean.length; i += chunkSize) {
5345
+ const chunk = clean.slice(i, i + chunkSize);
4612
5346
  slides.push({
5347
+ slideIndex: slides.length + 1,
4613
5348
  title: chunk[0] || `Slide ${Math.floor(i / chunkSize) + 1}`,
4614
- texts: chunk.slice(1)
5349
+ subtitle: chunk.length > 2 ? chunk[1] : void 0,
5350
+ paragraphs: chunk.length > 2 ? chunk.slice(2) : chunk.slice(1),
5351
+ tableColumns: [],
5352
+ pictureUrl: pictures[slides.length] || null
4615
5353
  });
4616
5354
  }
4617
5355
  }