@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/index.cjs CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  var DOMPurify2 = require('dompurify');
4
+ var pdfjsLib = require('pdfjs-dist');
4
5
  var docx = require('docx-preview');
5
6
  var fflate = require('fflate');
6
7
  var XLSX = require('xlsx');
@@ -11,7 +12,7 @@ var THREE = require('three');
11
12
  var STLLoader_js = require('three/examples/jsm/loaders/STLLoader.js');
12
13
  var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
13
14
  var OrbitControls_js = require('three/examples/jsm/controls/OrbitControls.js');
14
- var rtf_js = require('rtf.js');
15
+ var RTFJS = require('rtf.js/dist/RTFJS.bundle.js');
15
16
 
16
17
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
17
18
 
@@ -34,10 +35,12 @@ function _interopNamespace(e) {
34
35
  }
35
36
 
36
37
  var DOMPurify2__default = /*#__PURE__*/_interopDefault(DOMPurify2);
38
+ var pdfjsLib__namespace = /*#__PURE__*/_interopNamespace(pdfjsLib);
37
39
  var docx__namespace = /*#__PURE__*/_interopNamespace(docx);
38
40
  var XLSX__namespace = /*#__PURE__*/_interopNamespace(XLSX);
39
41
  var hljs__default = /*#__PURE__*/_interopDefault(hljs);
40
42
  var THREE__namespace = /*#__PURE__*/_interopNamespace(THREE);
43
+ var RTFJS__namespace = /*#__PURE__*/_interopNamespace(RTFJS);
41
44
 
42
45
  // ../core/dist/index.js
43
46
  var EventEmitter = class {
@@ -293,6 +296,39 @@ function detectOoxmlType(buffer) {
293
296
  }
294
297
  return "application/zip";
295
298
  }
299
+ function detectCfbfType(buffer) {
300
+ const bytes = new Uint8Array(buffer);
301
+ const hasUtf16le = (str) => {
302
+ const target = new Uint8Array(str.length * 2);
303
+ for (let i = 0; i < str.length; i++) {
304
+ target[i * 2] = str.charCodeAt(i);
305
+ target[i * 2 + 1] = 0;
306
+ }
307
+ const targetLen = target.length;
308
+ const max = bytes.length - targetLen;
309
+ for (let i = 0; i <= max; i++) {
310
+ let match = true;
311
+ for (let j = 0; j < targetLen; j++) {
312
+ if (bytes[i + j] !== target[j]) {
313
+ match = false;
314
+ break;
315
+ }
316
+ }
317
+ if (match) return true;
318
+ }
319
+ return false;
320
+ };
321
+ if (hasUtf16le("PowerPoint Document")) {
322
+ return { mime: "application/vnd.ms-powerpoint", extension: ".ppt" };
323
+ }
324
+ if (hasUtf16le("WordDocument")) {
325
+ return { mime: "application/msword", extension: ".doc" };
326
+ }
327
+ if (hasUtf16le("Workbook") || hasUtf16le("Book")) {
328
+ return { mime: "application/vnd.ms-excel", extension: ".xls" };
329
+ }
330
+ return null;
331
+ }
296
332
  function extractExtension(nameOrUrl) {
297
333
  try {
298
334
  const url = new URL(nameOrUrl);
@@ -357,6 +393,18 @@ async function sourceToArrayBuffer(source, signal) {
357
393
  } else {
358
394
  metadata.mimeType = metadata.mimeType ?? magicMime;
359
395
  }
396
+ } else if (magicMime === "application/x-cfbf") {
397
+ if (metadata.extension) {
398
+ metadata.mimeType = mimeFromExtension(metadata.extension) ?? magicMime;
399
+ } else {
400
+ const cfbf = detectCfbfType(buffer);
401
+ if (cfbf) {
402
+ metadata.mimeType = cfbf.mime;
403
+ metadata.extension = cfbf.extension;
404
+ } else {
405
+ metadata.mimeType = magicMime;
406
+ }
407
+ }
360
408
  } else {
361
409
  metadata.mimeType = magicMime;
362
410
  }
@@ -505,11 +553,21 @@ var ToolbarController = class {
505
553
  el;
506
554
  toolbarEl;
507
555
  actions = [];
556
+ pageInputEl = null;
557
+ pageLabelEl = null;
508
558
  constructor(container) {
509
559
  this.el = container;
510
560
  this.toolbarEl = createElement("div", { className: "fp-toolbar" });
511
561
  this.el.appendChild(this.toolbarEl);
512
562
  }
563
+ setPage(page, max) {
564
+ if (this.pageInputEl) {
565
+ this.pageInputEl.value = page.toString();
566
+ }
567
+ if (max !== void 0 && this.pageLabelEl) {
568
+ this.pageLabelEl.textContent = ` / ${max}`;
569
+ }
570
+ }
513
571
  update(actions) {
514
572
  this.actions = actions;
515
573
  this.render();
@@ -552,6 +610,7 @@ var ToolbarController = class {
552
610
  if (action.type === "separator") {
553
611
  groupEl.appendChild(createElement("div", { className: "fp-toolbar-separator" }));
554
612
  } else if (action.type === "page-nav") {
613
+ const max = action.max ?? 1;
555
614
  const prevBtn = this.createButton(
556
615
  "prev",
557
616
  ICON_PAGE_PREV,
@@ -570,7 +629,6 @@ var ToolbarController = class {
570
629
  "Next Page",
571
630
  () => {
572
631
  const cur = parseInt(input.value, 10) || 1;
573
- const max = action.max ?? 1;
574
632
  if (cur < max) {
575
633
  input.value = (cur + 1).toString();
576
634
  action.execute("next", cur + 1);
@@ -582,15 +640,18 @@ var ToolbarController = class {
582
640
  type: "number",
583
641
  value: (action.value ?? 1).toString(),
584
642
  min: "1",
585
- max: (action.max ?? 1).toString()
643
+ max: max.toString()
586
644
  });
587
645
  input.addEventListener("change", () => {
588
- const val = parseInt(input.value, 10);
589
- if (!isNaN(val)) {
590
- action.execute("go", val);
591
- }
646
+ let val = parseInt(input.value, 10);
647
+ if (isNaN(val)) val = 1;
648
+ val = Math.max(1, Math.min(max, val));
649
+ input.value = val.toString();
650
+ action.execute("go", val);
592
651
  });
593
- const label = createElement("span", { className: "fp-toolbar-label" }, ` / ${action.max ?? 1}`);
652
+ const label = createElement("span", { className: "fp-toolbar-label" }, ` / ${max}`);
653
+ this.pageInputEl = input;
654
+ this.pageLabelEl = label;
594
655
  groupEl.appendChild(prevBtn);
595
656
  groupEl.appendChild(input);
596
657
  groupEl.appendChild(label);
@@ -768,7 +829,13 @@ var FilePreviewViewer = class {
768
829
  buffer,
769
830
  options,
770
831
  signal,
771
- emit: (event, payload) => this.eventEmitter.emit(event, payload)
832
+ emit: (event, payload) => {
833
+ if (event === "page-change" && payload && typeof payload.page === "number") {
834
+ const total = payload.total ?? payload.totalPages;
835
+ this.toolbar?.setPage(payload.page, total);
836
+ }
837
+ this.eventEmitter.emit(event, payload);
838
+ }
772
839
  });
773
840
  this.activeInstance = instance;
774
841
  this.hideLoading();
@@ -802,6 +869,7 @@ var FilePreviewViewer = class {
802
869
  if (thumbnails && thumbnails.length > 0 && this.thumbnailPanel) {
803
870
  this.thumbnailPanel.update(thumbnails, (index) => {
804
871
  instance.goToPage?.(index + 1);
872
+ this.toolbar?.setPage(index + 1);
805
873
  });
806
874
  if (options.showThumbnails) {
807
875
  this.thumbnailPanel.show();
@@ -927,9 +995,13 @@ var FilePreviewViewer = class {
927
995
  if (e.key === "ArrowRight" || e.key === "PageDown") {
928
996
  const cur = this.activeInstance.getCurrentPage?.() ?? 1;
929
997
  this.activeInstance.goToPage?.(cur + 1);
998
+ const nextCur = this.activeInstance.getCurrentPage?.() ?? cur + 1;
999
+ this.toolbar?.setPage(nextCur);
930
1000
  } else if (e.key === "ArrowLeft" || e.key === "PageUp") {
931
1001
  const cur = this.activeInstance.getCurrentPage?.() ?? 1;
932
1002
  this.activeInstance.goToPage?.(Math.max(1, cur - 1));
1003
+ const prevCur = this.activeInstance.getCurrentPage?.() ?? Math.max(1, cur - 1);
1004
+ this.toolbar?.setPage(prevCur);
933
1005
  } else if (e.key === "+" || e.key === "=") {
934
1006
  this.activeInstance.zoomIn?.();
935
1007
  } else if (e.key === "-" || e.key === "_") {
@@ -1195,30 +1267,62 @@ var CfbfReader = class {
1195
1267
  return result.subarray(0, targetSize);
1196
1268
  }
1197
1269
  };
1198
-
1199
- // ../plugins/pdf/dist/index.js
1270
+ if (typeof window !== "undefined" && pdfjsLib__namespace.GlobalWorkerOptions) {
1271
+ if (!pdfjsLib__namespace.GlobalWorkerOptions.workerSrc) {
1272
+ pdfjsLib__namespace.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib__namespace.version || "4.10.38"}/build/pdf.worker.min.mjs`;
1273
+ }
1274
+ }
1200
1275
  var PdfPlugin = class {
1201
1276
  id = "pdf";
1202
- name = "PDF Preview";
1277
+ name = "PDF Document Preview";
1203
1278
  extensions = [".pdf"];
1204
1279
  mimeTypes = ["application/pdf"];
1205
1280
  weight = 100;
1206
1281
  supports(file) {
1207
1282
  const ext = file.metadata.extension?.toLowerCase();
1208
1283
  const mime = file.metadata.mimeType?.toLowerCase();
1209
- return ext === ".pdf" || mime === "application/pdf";
1284
+ if (ext) return this.extensions.includes(ext);
1285
+ return this.mimeTypes.includes(mime || "");
1210
1286
  }
1211
1287
  getToolbarActions(instance) {
1288
+ const totalPages = instance.getPageCount?.() ?? 1;
1289
+ const curPage = instance.getCurrentPage?.() ?? 1;
1212
1290
  return [
1291
+ {
1292
+ id: "thumbnails",
1293
+ icon: "thumbnails",
1294
+ label: "Page Thumbnails",
1295
+ type: "button",
1296
+ group: "navigation",
1297
+ execute: () => instance.toggleThumbnails?.()
1298
+ },
1299
+ {
1300
+ id: "page-nav",
1301
+ icon: "",
1302
+ label: "Page Navigation",
1303
+ type: "page-nav",
1304
+ group: "navigation",
1305
+ value: curPage,
1306
+ max: totalPages,
1307
+ execute: (action, page) => {
1308
+ const cur = instance.getCurrentPage?.() ?? 1;
1309
+ const max = instance.getPageCount?.() ?? 1;
1310
+ if (action === "prev") {
1311
+ if (cur > 1) instance.goToPage?.(cur - 1);
1312
+ } else if (action === "next") {
1313
+ if (cur < max) instance.goToPage?.(cur + 1);
1314
+ } else if (typeof page === "number") {
1315
+ instance.goToPage?.(page);
1316
+ }
1317
+ }
1318
+ },
1213
1319
  {
1214
1320
  id: "zoom-out",
1215
1321
  icon: "zoom-out",
1216
1322
  label: "Zoom Out",
1217
1323
  type: "button",
1218
1324
  group: "zoom",
1219
- execute: () => {
1220
- instance.zoomOut?.();
1221
- }
1325
+ execute: () => instance.zoomOut?.()
1222
1326
  },
1223
1327
  {
1224
1328
  id: "zoom-in",
@@ -1226,9 +1330,7 @@ var PdfPlugin = class {
1226
1330
  label: "Zoom In",
1227
1331
  type: "button",
1228
1332
  group: "zoom",
1229
- execute: () => {
1230
- instance.zoomIn?.();
1231
- }
1333
+ execute: () => instance.zoomIn?.()
1232
1334
  },
1233
1335
  {
1234
1336
  id: "fit-page",
@@ -1236,39 +1338,23 @@ var PdfPlugin = class {
1236
1338
  label: "Fit to Page",
1237
1339
  type: "button",
1238
1340
  group: "zoom",
1239
- execute: () => {
1240
- instance.fitToPage?.();
1241
- }
1341
+ execute: () => instance.fitToPage?.()
1242
1342
  },
1243
1343
  {
1244
1344
  id: "rotate-cw",
1245
1345
  icon: "rotate-cw",
1246
- label: "Rotate",
1346
+ label: "Rotate Clockwise",
1247
1347
  type: "button",
1248
1348
  group: "view",
1249
- execute: () => {
1250
- instance.rotateCW?.();
1251
- }
1252
- },
1253
- {
1254
- id: "page-nav",
1255
- icon: "page-nav",
1256
- label: "Page Navigation",
1257
- type: "page-nav",
1258
- group: "navigation",
1259
- execute: (page) => {
1260
- if (typeof page === "number") instance.goToPage?.(page);
1261
- }
1349
+ execute: () => instance.rotateCW?.()
1262
1350
  },
1263
1351
  {
1264
1352
  id: "download",
1265
1353
  icon: "download",
1266
- label: "Download",
1354
+ label: "Download PDF",
1267
1355
  type: "button",
1268
1356
  group: "actions",
1269
- execute: () => {
1270
- instance.download?.();
1271
- }
1357
+ execute: () => instance.download?.()
1272
1358
  },
1273
1359
  {
1274
1360
  id: "print",
@@ -1276,99 +1362,213 @@ var PdfPlugin = class {
1276
1362
  label: "Print",
1277
1363
  type: "button",
1278
1364
  group: "actions",
1279
- execute: () => {
1280
- instance.print?.();
1281
- }
1365
+ execute: () => instance.print?.()
1282
1366
  }
1283
1367
  ];
1284
1368
  }
1285
1369
  async render(ctx) {
1286
- const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1287
- const url = URL.createObjectURL(blob);
1288
- const wrapper = document.createElement("div");
1289
- wrapper.style.width = "100%";
1290
- wrapper.style.height = "100%";
1291
- wrapper.style.overflow = "hidden";
1292
- wrapper.style.display = "flex";
1293
- wrapper.style.justifyContent = "center";
1294
- wrapper.style.alignItems = "center";
1295
- const iframe = document.createElement("iframe");
1296
- iframe.src = url;
1297
- iframe.style.width = "100%";
1298
- iframe.style.height = "100%";
1299
- iframe.style.border = "none";
1300
- wrapper.appendChild(iframe);
1301
- ctx.container.appendChild(wrapper);
1370
+ const container = document.createElement("div");
1371
+ container.className = "fp-pdf-container";
1372
+ container.style.width = "100%";
1373
+ container.style.height = "100%";
1374
+ container.style.overflow = "auto";
1375
+ container.style.display = "flex";
1376
+ container.style.flexDirection = "column";
1377
+ container.style.alignItems = "center";
1378
+ container.style.padding = "24px 16px";
1379
+ container.style.backgroundColor = "#0f172a";
1380
+ container.style.boxSizing = "border-box";
1381
+ container.style.position = "relative";
1382
+ const pageCard = document.createElement("div");
1383
+ pageCard.className = "fp-pdf-page-card";
1384
+ pageCard.style.boxShadow = "0 10px 35px rgba(0, 0, 0, 0.5)";
1385
+ pageCard.style.backgroundColor = "#ffffff";
1386
+ pageCard.style.borderRadius = "4px";
1387
+ pageCard.style.overflow = "hidden";
1388
+ pageCard.style.lineHeight = "0";
1389
+ pageCard.style.transition = "transform 0.15s ease";
1390
+ pageCard.style.position = "relative";
1391
+ const canvas = document.createElement("canvas");
1392
+ pageCard.appendChild(canvas);
1393
+ container.appendChild(pageCard);
1394
+ const indicator = document.createElement("div");
1395
+ indicator.className = "fp-pdf-page-indicator";
1396
+ indicator.style.position = "sticky";
1397
+ indicator.style.bottom = "16px";
1398
+ indicator.style.marginTop = "16px";
1399
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
1400
+ indicator.style.backdropFilter = "blur(8px)";
1401
+ indicator.style.color = "#f8fafc";
1402
+ indicator.style.fontSize = "12px";
1403
+ indicator.style.fontWeight = "600";
1404
+ indicator.style.padding = "5px 14px";
1405
+ indicator.style.borderRadius = "20px";
1406
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
1407
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
1408
+ indicator.style.zIndex = "10";
1409
+ indicator.style.userSelect = "none";
1410
+ indicator.style.pointerEvents = "none";
1411
+ container.appendChild(indicator);
1412
+ ctx.container.appendChild(container);
1413
+ const loadingTask = pdfjsLib__namespace.getDocument({
1414
+ data: new Uint8Array(ctx.buffer),
1415
+ cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib__namespace.version || "4.10.38"}/cmaps/`,
1416
+ cMapPacked: true,
1417
+ standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib__namespace.version || "4.10.38"}/standard_fonts/`
1418
+ });
1419
+ const pdfDoc = await loadingTask.promise;
1420
+ const totalPages = Math.max(1, pdfDoc.numPages);
1302
1421
  let currentPage = 1;
1303
- let currentZoom = 1;
1422
+ let zoomScale = 1;
1304
1423
  let rotation = 0;
1424
+ let currentRenderTask = null;
1425
+ const renderPage = async (pageNum) => {
1426
+ if (currentRenderTask) {
1427
+ try {
1428
+ currentRenderTask.cancel();
1429
+ } catch {
1430
+ }
1431
+ currentRenderTask = null;
1432
+ }
1433
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
1434
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
1435
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
1436
+ const page = await pdfDoc.getPage(currentPage);
1437
+ const containerWidth = container.clientWidth || 900;
1438
+ const unscaledVp = page.getViewport({ scale: 1, rotation });
1439
+ const baseScale = Math.min((containerWidth - 64) / unscaledVp.width, 1.6);
1440
+ const effectiveScale = (baseScale > 0 ? baseScale : 1) * zoomScale;
1441
+ const pixelRatio = window.devicePixelRatio || 1;
1442
+ const viewport = page.getViewport({ scale: effectiveScale, rotation });
1443
+ canvas.width = Math.floor(viewport.width * pixelRatio);
1444
+ canvas.height = Math.floor(viewport.height * pixelRatio);
1445
+ canvas.style.width = `${Math.floor(viewport.width)}px`;
1446
+ canvas.style.height = `${Math.floor(viewport.height)}px`;
1447
+ const canvasCtx = canvas.getContext("2d");
1448
+ if (!canvasCtx) return;
1449
+ canvasCtx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
1450
+ currentRenderTask = page.render({
1451
+ canvasContext: canvasCtx,
1452
+ viewport
1453
+ });
1454
+ try {
1455
+ await currentRenderTask.promise;
1456
+ } catch (err) {
1457
+ if (err?.name !== "RenderingCancelledException") {
1458
+ console.warn("[PdfPlugin] Page render warning:", err);
1459
+ }
1460
+ } finally {
1461
+ currentRenderTask = null;
1462
+ }
1463
+ };
1464
+ await renderPage(1);
1305
1465
  const cleanup = () => {
1306
- URL.revokeObjectURL(url);
1307
- wrapper.remove();
1466
+ if (currentRenderTask) {
1467
+ try {
1468
+ currentRenderTask.cancel();
1469
+ } catch {
1470
+ }
1471
+ }
1472
+ try {
1473
+ pdfDoc.destroy();
1474
+ } catch {
1475
+ }
1476
+ container.remove();
1308
1477
  ctx.container.innerHTML = "";
1309
1478
  };
1310
1479
  ctx.signal.addEventListener("abort", cleanup);
1311
- return {
1480
+ const instance = {
1312
1481
  destroy: cleanup,
1313
1482
  zoomIn: () => {
1314
- currentZoom += 0.1;
1315
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1483
+ zoomScale = Math.min(3.5, zoomScale + 0.2);
1484
+ renderPage(currentPage);
1316
1485
  },
1317
1486
  zoomOut: () => {
1318
- currentZoom = Math.max(0.2, currentZoom - 0.1);
1319
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1487
+ zoomScale = Math.max(0.3, zoomScale - 0.2);
1488
+ renderPage(currentPage);
1320
1489
  },
1321
- getZoom: () => currentZoom,
1490
+ getZoom: () => zoomScale,
1322
1491
  setZoom: (level) => {
1323
- currentZoom = level;
1324
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1492
+ zoomScale = Math.max(0.3, Math.min(3.5, level));
1493
+ renderPage(currentPage);
1325
1494
  },
1326
1495
  fitToPage: () => {
1327
- currentZoom = 1;
1328
- iframe.style.transform = `scale(1) rotate(${rotation}deg)`;
1496
+ zoomScale = 1;
1497
+ renderPage(currentPage);
1329
1498
  },
1330
1499
  rotateCW: () => {
1331
1500
  rotation = (rotation + 90) % 360;
1332
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1501
+ renderPage(currentPage);
1333
1502
  },
1334
1503
  rotateCCW: () => {
1335
1504
  rotation = (rotation - 90 + 360) % 360;
1336
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1505
+ renderPage(currentPage);
1337
1506
  },
1338
1507
  getRotation: () => rotation,
1508
+ getPageCount: () => totalPages,
1509
+ getCurrentPage: () => currentPage,
1339
1510
  goToPage: (page) => {
1340
- currentPage = page;
1341
- iframe.src = `${url}#page=${page}`;
1511
+ renderPage(page);
1342
1512
  },
1343
- getCurrentPage: () => currentPage,
1344
1513
  download: () => {
1514
+ const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1515
+ const url = URL.createObjectURL(blob);
1345
1516
  const a = document.createElement("a");
1346
1517
  a.href = url;
1347
1518
  a.download = ctx.metadata.name || "document.pdf";
1348
1519
  a.click();
1520
+ URL.revokeObjectURL(url);
1349
1521
  },
1350
1522
  print: () => {
1351
- iframe.contentWindow?.print();
1523
+ const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1524
+ const url = URL.createObjectURL(blob);
1525
+ const hiddenIframe = document.createElement("iframe");
1526
+ hiddenIframe.style.position = "fixed";
1527
+ hiddenIframe.style.right = "0";
1528
+ hiddenIframe.style.bottom = "0";
1529
+ hiddenIframe.style.width = "0";
1530
+ hiddenIframe.style.height = "0";
1531
+ hiddenIframe.style.border = "0";
1532
+ document.body.appendChild(hiddenIframe);
1533
+ hiddenIframe.src = url;
1534
+ hiddenIframe.onload = () => {
1535
+ setTimeout(() => {
1536
+ hiddenIframe.contentWindow?.print();
1537
+ setTimeout(() => {
1538
+ hiddenIframe.remove();
1539
+ URL.revokeObjectURL(url);
1540
+ }, 1e3);
1541
+ }, 300);
1542
+ };
1352
1543
  },
1353
1544
  getThumbnails: async () => {
1354
- return [
1355
- {
1356
- index: 1,
1357
- label: "Page 1",
1358
- render: async (canvas) => {
1359
- const context = canvas.getContext("2d");
1360
- if (context) {
1361
- context.fillStyle = "#fff";
1362
- context.fillRect(0, 0, canvas.width, canvas.height);
1363
- context.fillStyle = "#333";
1364
- context.font = "12px sans-serif";
1365
- context.fillText("PDF Preview", 10, 20);
1545
+ const thumbnails = [];
1546
+ const count = Math.min(totalPages, 50);
1547
+ for (let i = 1; i <= count; i++) {
1548
+ thumbnails.push({
1549
+ index: i,
1550
+ label: `Page ${i}`,
1551
+ render: async (thumbCanvas) => {
1552
+ try {
1553
+ const p = await pdfDoc.getPage(i);
1554
+ const baseVp = p.getViewport({ scale: 1 });
1555
+ const thumbScale = (thumbCanvas.width || 120) / baseVp.width;
1556
+ const thumbVp = p.getViewport({ scale: thumbScale });
1557
+ thumbCanvas.height = Math.floor(thumbVp.height);
1558
+ const tCtx = thumbCanvas.getContext("2d");
1559
+ if (tCtx) {
1560
+ await p.render({ canvasContext: tCtx, viewport: thumbVp }).promise;
1561
+ }
1562
+ } catch (e) {
1563
+ console.warn(`[PdfPlugin] Error generating thumbnail for page ${i}:`, e);
1366
1564
  }
1367
1565
  }
1368
- }
1369
- ];
1566
+ });
1567
+ }
1568
+ return thumbnails;
1370
1569
  }
1371
1570
  };
1571
+ return instance;
1372
1572
  }
1373
1573
  };
1374
1574
  function pdfPlugin() {
@@ -1689,10 +1889,37 @@ var DocxPlugin = class {
1689
1889
  supports(file) {
1690
1890
  const ext = file.metadata.extension?.toLowerCase();
1691
1891
  const mime = file.metadata.mimeType?.toLowerCase();
1692
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
1892
+ if (ext) {
1893
+ return this.extensions.includes(ext);
1894
+ }
1895
+ return this.mimeTypes.includes(mime || "");
1693
1896
  }
1694
1897
  getToolbarActions(instance) {
1695
- return [
1898
+ const totalPages = instance.getPageCount?.() ?? 1;
1899
+ const actions = [];
1900
+ if (totalPages > 1) {
1901
+ actions.push({
1902
+ id: "page-nav",
1903
+ icon: "",
1904
+ label: "Page Navigation",
1905
+ type: "page-nav",
1906
+ group: "navigation",
1907
+ value: instance.getCurrentPage?.() ?? 1,
1908
+ max: totalPages,
1909
+ execute: (action, page) => {
1910
+ const cur = instance.getCurrentPage?.() ?? 1;
1911
+ const max = instance.getPageCount?.() ?? 1;
1912
+ if (action === "prev") {
1913
+ if (cur > 1) instance.goToPage?.(cur - 1);
1914
+ } else if (action === "next") {
1915
+ if (cur < max) instance.goToPage?.(cur + 1);
1916
+ } else if (typeof page === "number") {
1917
+ instance.goToPage?.(page);
1918
+ }
1919
+ }
1920
+ });
1921
+ }
1922
+ actions.push(
1696
1923
  {
1697
1924
  id: "zoom-out",
1698
1925
  icon: "zoom-out",
@@ -1733,7 +1960,8 @@ var DocxPlugin = class {
1733
1960
  group: "actions",
1734
1961
  execute: () => instance.print?.()
1735
1962
  }
1736
- ];
1963
+ );
1964
+ return actions;
1737
1965
  }
1738
1966
  async render(ctx) {
1739
1967
  const wrapper = document.createElement("div");
@@ -1809,7 +2037,51 @@ var DocxPlugin = class {
1809
2037
  }
1810
2038
  }
1811
2039
  }
2040
+ const sections = wrapper.querySelectorAll("section.docx");
2041
+ const cards = wrapper.querySelectorAll(".fp-docx-page-card");
2042
+ const pageElements = sections.length > 0 ? sections : cards;
2043
+ const totalPages = Math.max(1, pageElements.length);
2044
+ let currentPage = 1;
2045
+ let indicator = null;
2046
+ if (totalPages > 1) {
2047
+ indicator = document.createElement("div");
2048
+ indicator.className = "fp-docx-page-indicator";
2049
+ indicator.style.position = "sticky";
2050
+ indicator.style.bottom = "16px";
2051
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
2052
+ indicator.style.backdropFilter = "blur(8px)";
2053
+ indicator.style.color = "#f8fafc";
2054
+ indicator.style.fontSize = "12px";
2055
+ indicator.style.fontWeight = "600";
2056
+ indicator.style.padding = "5px 14px";
2057
+ indicator.style.borderRadius = "20px";
2058
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
2059
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
2060
+ indicator.style.zIndex = "10";
2061
+ indicator.style.userSelect = "none";
2062
+ indicator.style.pointerEvents = "none";
2063
+ indicator.style.textAlign = "center";
2064
+ indicator.style.width = "fit-content";
2065
+ indicator.style.margin = "16px auto 0";
2066
+ ctx.container.appendChild(indicator);
2067
+ }
2068
+ const showPage = (pageNum) => {
2069
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
2070
+ if (pageElements.length > 1) {
2071
+ pageElements.forEach((sec, idx) => {
2072
+ sec.style.display = idx + 1 === currentPage ? "block" : "none";
2073
+ });
2074
+ }
2075
+ if (indicator) {
2076
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
2077
+ }
2078
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
2079
+ };
2080
+ if (totalPages > 1) {
2081
+ showPage(1);
2082
+ }
1812
2083
  const cleanup = () => {
2084
+ indicator?.remove();
1813
2085
  for (const url of createdBlobUrls) {
1814
2086
  URL.revokeObjectURL(url);
1815
2087
  }
@@ -1820,6 +2092,11 @@ var DocxPlugin = class {
1820
2092
  ctx.signal.addEventListener("abort", cleanup);
1821
2093
  return {
1822
2094
  destroy: cleanup,
2095
+ getPageCount: () => totalPages,
2096
+ getCurrentPage: () => currentPage,
2097
+ goToPage: (page) => {
2098
+ showPage(page);
2099
+ },
1823
2100
  zoomIn: () => {
1824
2101
  scale += 0.1;
1825
2102
  wrapper.style.transform = `scale(${scale})`;
@@ -2078,10 +2355,37 @@ var ExcelPlugin = class {
2078
2355
  supports(file) {
2079
2356
  const ext = file.metadata.extension?.toLowerCase();
2080
2357
  const mime = file.metadata.mimeType?.toLowerCase();
2081
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
2358
+ if (ext) {
2359
+ return this.extensions.includes(ext);
2360
+ }
2361
+ return this.mimeTypes.includes(mime || "");
2082
2362
  }
2083
2363
  getToolbarActions(instance) {
2084
- return [
2364
+ const totalSheets = instance.getPageCount?.() ?? 1;
2365
+ const actions = [];
2366
+ if (totalSheets > 1) {
2367
+ actions.push({
2368
+ id: "page-nav",
2369
+ icon: "",
2370
+ label: "Sheet Navigation",
2371
+ type: "page-nav",
2372
+ group: "navigation",
2373
+ value: instance.getCurrentPage?.() ?? 1,
2374
+ max: totalSheets,
2375
+ execute: (action, sheet) => {
2376
+ const cur = instance.getCurrentPage?.() ?? 1;
2377
+ const max = instance.getPageCount?.() ?? 1;
2378
+ if (action === "prev") {
2379
+ if (cur > 1) instance.goToPage?.(cur - 1);
2380
+ } else if (action === "next") {
2381
+ if (cur < max) instance.goToPage?.(cur + 1);
2382
+ } else if (typeof sheet === "number") {
2383
+ instance.goToPage?.(sheet);
2384
+ }
2385
+ }
2386
+ });
2387
+ }
2388
+ actions.push(
2085
2389
  {
2086
2390
  id: "zoom-out",
2087
2391
  icon: "zoom-out",
@@ -2102,16 +2406,6 @@ var ExcelPlugin = class {
2102
2406
  instance.zoomIn?.();
2103
2407
  }
2104
2408
  },
2105
- {
2106
- id: "page-nav",
2107
- icon: "page-nav",
2108
- label: "Sheet Navigation",
2109
- type: "page-nav",
2110
- group: "navigation",
2111
- execute: (sheet) => {
2112
- if (typeof sheet === "number") instance.goToPage?.(sheet);
2113
- }
2114
- },
2115
2409
  {
2116
2410
  id: "download",
2117
2411
  icon: "download",
@@ -2132,7 +2426,8 @@ var ExcelPlugin = class {
2132
2426
  instance.print?.();
2133
2427
  }
2134
2428
  }
2135
- ];
2429
+ );
2430
+ return actions;
2136
2431
  }
2137
2432
  async render(ctx) {
2138
2433
  const container = document.createElement("div");
@@ -2216,6 +2511,7 @@ var ExcelPlugin = class {
2216
2511
  b.style.fontWeight = "normal";
2217
2512
  }
2218
2513
  });
2514
+ ctx.emit("page-change", { page: currentSheetIndex, total: sheetNames.length });
2219
2515
  };
2220
2516
  if (sheetNames.length > 0) {
2221
2517
  sheetNames.forEach((name, idx) => {
@@ -2465,7 +2761,31 @@ var CodePlugin = class {
2465
2761
  return false;
2466
2762
  }
2467
2763
  getToolbarActions(instance) {
2468
- return [
2764
+ const totalPages = instance.getPageCount?.() ?? 1;
2765
+ const actions = [];
2766
+ if (totalPages > 1) {
2767
+ actions.push({
2768
+ id: "page-nav",
2769
+ icon: "",
2770
+ label: "Page Navigation",
2771
+ type: "page-nav",
2772
+ group: "navigation",
2773
+ value: instance.getCurrentPage?.() ?? 1,
2774
+ max: totalPages,
2775
+ execute: (action, page) => {
2776
+ const cur = instance.getCurrentPage?.() ?? 1;
2777
+ const max = instance.getPageCount?.() ?? 1;
2778
+ if (action === "prev") {
2779
+ if (cur > 1) instance.goToPage?.(cur - 1);
2780
+ } else if (action === "next") {
2781
+ if (cur < max) instance.goToPage?.(cur + 1);
2782
+ } else if (typeof page === "number") {
2783
+ instance.goToPage?.(page);
2784
+ }
2785
+ }
2786
+ });
2787
+ }
2788
+ actions.push(
2469
2789
  {
2470
2790
  id: "zoom-out",
2471
2791
  icon: "zoom-out",
@@ -2516,11 +2836,16 @@ var CodePlugin = class {
2516
2836
  instance.print?.();
2517
2837
  }
2518
2838
  }
2519
- ];
2839
+ );
2840
+ return actions;
2520
2841
  }
2521
2842
  async render(ctx) {
2522
2843
  const decoder = new TextDecoder("utf-8");
2523
- const text = decoder.decode(ctx.buffer);
2844
+ const fullText = decoder.decode(ctx.buffer);
2845
+ const pageSplitRegex = /(?:\f|\x0C|(?:\r?\n|^)\s*[-=_]{3,}\s*(?:PAGE|Page|page break|Page Break)[\s\d\w-]*[-=_]{3,}\s*(?:\r?\n|$))/i;
2846
+ const rawPages = fullText.split(pageSplitRegex).map((p) => p.trim()).filter((p) => p.length > 0);
2847
+ const totalPages = Math.max(1, rawPages.length);
2848
+ let currentPage = 1;
2524
2849
  const container = document.createElement("div");
2525
2850
  container.style.width = "100%";
2526
2851
  container.style.height = "100%";
@@ -2539,25 +2864,66 @@ var CodePlugin = class {
2539
2864
  pre.style.wordBreak = "break-all";
2540
2865
  const code = document.createElement("code");
2541
2866
  const ext = (ctx.metadata.extension || "").replace(".", "");
2542
- try {
2543
- if (ext && hljs__default.default.getLanguage(ext)) {
2544
- code.innerHTML = hljs__default.default.highlight(text, { language: ext }).value;
2545
- } else {
2546
- code.innerHTML = hljs__default.default.highlightAuto(text).value;
2867
+ const renderCodePage = (text) => {
2868
+ try {
2869
+ if (ext && hljs__default.default.getLanguage(ext)) {
2870
+ code.innerHTML = hljs__default.default.highlight(text, { language: ext }).value;
2871
+ } else {
2872
+ code.innerHTML = hljs__default.default.highlightAuto(text).value;
2873
+ }
2874
+ } catch {
2875
+ code.textContent = text;
2547
2876
  }
2548
- } catch {
2549
- code.textContent = text;
2550
- }
2877
+ };
2878
+ renderCodePage(rawPages[0] || fullText);
2551
2879
  pre.appendChild(code);
2552
2880
  container.appendChild(pre);
2881
+ let indicator = null;
2882
+ if (totalPages > 1) {
2883
+ indicator = document.createElement("div");
2884
+ indicator.className = "fp-code-page-indicator";
2885
+ indicator.style.position = "sticky";
2886
+ indicator.style.bottom = "16px";
2887
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
2888
+ indicator.style.backdropFilter = "blur(8px)";
2889
+ indicator.style.color = "#f8fafc";
2890
+ indicator.style.fontSize = "12px";
2891
+ indicator.style.fontWeight = "600";
2892
+ indicator.style.padding = "5px 14px";
2893
+ indicator.style.borderRadius = "20px";
2894
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
2895
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
2896
+ indicator.style.zIndex = "10";
2897
+ indicator.style.userSelect = "none";
2898
+ indicator.style.pointerEvents = "none";
2899
+ indicator.style.textAlign = "center";
2900
+ indicator.style.width = "fit-content";
2901
+ indicator.style.margin = "16px auto 0";
2902
+ container.appendChild(indicator);
2903
+ }
2904
+ const showPage = (pageNum) => {
2905
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
2906
+ renderCodePage(rawPages[currentPage - 1] || fullText);
2907
+ if (indicator) {
2908
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
2909
+ }
2910
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
2911
+ };
2912
+ if (totalPages > 1) {
2913
+ showPage(1);
2914
+ }
2553
2915
  ctx.container.appendChild(container);
2554
2916
  const cleanup = () => {
2917
+ indicator?.remove();
2555
2918
  container.remove();
2556
2919
  ctx.container.innerHTML = "";
2557
2920
  };
2558
2921
  ctx.signal.addEventListener("abort", cleanup);
2559
2922
  return {
2560
2923
  destroy: cleanup,
2924
+ getPageCount: () => totalPages,
2925
+ getCurrentPage: () => currentPage,
2926
+ goToPage: (page) => showPage(page),
2561
2927
  zoomIn: () => {
2562
2928
  fontSize = Math.min(32, fontSize + 2);
2563
2929
  pre.style.fontSize = `${fontSize}px`;
@@ -2585,7 +2951,7 @@ var CodePlugin = class {
2585
2951
  window.print();
2586
2952
  },
2587
2953
  copy: () => {
2588
- navigator.clipboard?.writeText(text);
2954
+ navigator.clipboard?.writeText(fullText);
2589
2955
  }
2590
2956
  };
2591
2957
  }
@@ -3020,7 +3386,10 @@ var PptxPlugin = class {
3020
3386
  supports(file) {
3021
3387
  const ext = file.metadata.extension?.toLowerCase();
3022
3388
  const mime = file.metadata.mimeType?.toLowerCase();
3023
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3389
+ if (ext) {
3390
+ return this.extensions.includes(ext);
3391
+ }
3392
+ return this.mimeTypes.includes(mime || "");
3024
3393
  }
3025
3394
  getToolbarActions(instance) {
3026
3395
  return [
@@ -3404,7 +3773,31 @@ var RtfPlugin = class {
3404
3773
  return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3405
3774
  }
3406
3775
  getToolbarActions(instance) {
3407
- return [
3776
+ const totalPages = instance.getPageCount?.() ?? 1;
3777
+ const actions = [];
3778
+ if (totalPages > 1) {
3779
+ actions.push({
3780
+ id: "page-nav",
3781
+ icon: "",
3782
+ label: "Page Navigation",
3783
+ type: "page-nav",
3784
+ group: "navigation",
3785
+ value: instance.getCurrentPage?.() ?? 1,
3786
+ max: totalPages,
3787
+ execute: (action, page) => {
3788
+ const cur = instance.getCurrentPage?.() ?? 1;
3789
+ const max = instance.getPageCount?.() ?? 1;
3790
+ if (action === "prev") {
3791
+ if (cur > 1) instance.goToPage?.(cur - 1);
3792
+ } else if (action === "next") {
3793
+ if (cur < max) instance.goToPage?.(cur + 1);
3794
+ } else if (typeof page === "number") {
3795
+ instance.goToPage?.(page);
3796
+ }
3797
+ }
3798
+ });
3799
+ }
3800
+ actions.push(
3408
3801
  {
3409
3802
  id: "zoom-out",
3410
3803
  icon: "zoom-out",
@@ -3445,7 +3838,8 @@ var RtfPlugin = class {
3445
3838
  group: "actions",
3446
3839
  execute: () => instance.print?.()
3447
3840
  }
3448
- ];
3841
+ );
3842
+ return actions;
3449
3843
  }
3450
3844
  async render(ctx) {
3451
3845
  const wrapper = document.createElement("div");
@@ -3464,25 +3858,82 @@ var RtfPlugin = class {
3464
3858
  ctx.container.style.backgroundColor = "#f1f5f9";
3465
3859
  ctx.container.appendChild(wrapper);
3466
3860
  let scale = 1;
3861
+ let pageElements = [];
3467
3862
  try {
3468
- const doc = new rtf_js.RTFJS.Document(ctx.buffer, {});
3863
+ if (typeof RTFJS__namespace.loggingEnabled === "function") {
3864
+ RTFJS__namespace.loggingEnabled(false);
3865
+ }
3866
+ const doc = new RTFJS__namespace.Document(ctx.buffer, {});
3469
3867
  const htmlElements = await doc.render();
3470
- for (const el of htmlElements) {
3868
+ pageElements = htmlElements;
3869
+ for (let i = 0; i < htmlElements.length; i++) {
3870
+ const el = htmlElements[i];
3871
+ el.style.display = i === 0 ? "block" : "none";
3471
3872
  wrapper.appendChild(el);
3472
3873
  }
3473
3874
  } catch (err) {
3474
3875
  console.warn("[RtfPlugin] RTF render error, fallback text:", err);
3475
3876
  const text = new TextDecoder("latin1").decode(ctx.buffer);
3476
3877
  const clean = text.replace(/\\par[d]?/g, "\n").replace(/\\[a-zA-Z0-9\-]+/g, "").replace(/[{}]/g, "");
3477
- wrapper.innerHTML = `<pre style="white-space: pre-wrap; font-family: serif; color: #333;">${clean}</pre>`;
3878
+ const pre = document.createElement("pre");
3879
+ pre.style.whiteSpace = "pre-wrap";
3880
+ pre.style.fontFamily = "serif";
3881
+ pre.style.color = "#333";
3882
+ pre.textContent = clean;
3883
+ wrapper.appendChild(pre);
3884
+ pageElements = [pre];
3885
+ }
3886
+ const totalPages = Math.max(1, pageElements.length);
3887
+ let currentPage = 1;
3888
+ let indicator = null;
3889
+ if (totalPages > 1) {
3890
+ indicator = document.createElement("div");
3891
+ indicator.className = "fp-rtf-page-indicator";
3892
+ indicator.style.position = "sticky";
3893
+ indicator.style.bottom = "16px";
3894
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
3895
+ indicator.style.backdropFilter = "blur(8px)";
3896
+ indicator.style.color = "#f8fafc";
3897
+ indicator.style.fontSize = "12px";
3898
+ indicator.style.fontWeight = "600";
3899
+ indicator.style.padding = "5px 14px";
3900
+ indicator.style.borderRadius = "20px";
3901
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
3902
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
3903
+ indicator.style.zIndex = "10";
3904
+ indicator.style.userSelect = "none";
3905
+ indicator.style.pointerEvents = "none";
3906
+ indicator.style.textAlign = "center";
3907
+ indicator.style.width = "fit-content";
3908
+ indicator.style.margin = "16px auto 0";
3909
+ ctx.container.appendChild(indicator);
3910
+ }
3911
+ const showPage = (pageNum) => {
3912
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
3913
+ if (totalPages > 1) {
3914
+ pageElements.forEach((el, idx) => {
3915
+ el.style.display = idx + 1 === currentPage ? "block" : "none";
3916
+ });
3917
+ }
3918
+ if (indicator) {
3919
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
3920
+ }
3921
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
3922
+ };
3923
+ if (totalPages > 1) {
3924
+ showPage(1);
3478
3925
  }
3479
3926
  const cleanup = () => {
3927
+ indicator?.remove();
3480
3928
  wrapper.remove();
3481
3929
  ctx.container.innerHTML = "";
3482
3930
  };
3483
3931
  ctx.signal.addEventListener("abort", cleanup);
3484
3932
  return {
3485
3933
  destroy: cleanup,
3934
+ getPageCount: () => totalPages,
3935
+ getCurrentPage: () => currentPage,
3936
+ goToPage: (page) => showPage(page),
3486
3937
  zoomIn: () => {
3487
3938
  scale += 0.1;
3488
3939
  wrapper.style.transform = `scale(${scale})`;
@@ -3663,45 +4114,48 @@ var OpenDocumentPlugin = class {
3663
4114
  supports(file) {
3664
4115
  const ext = file.metadata.extension?.toLowerCase();
3665
4116
  const mime = file.metadata.mimeType?.toLowerCase();
3666
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4117
+ if (ext) {
4118
+ return this.extensions.includes(ext);
4119
+ }
4120
+ return this.mimeTypes.includes(mime || "");
3667
4121
  }
3668
4122
  getToolbarActions(instance) {
3669
4123
  const isPresentation = instance.isPresentation;
4124
+ const totalPages = instance.getPageCount?.() ?? 1;
3670
4125
  const actions = [];
3671
- if (isPresentation) {
3672
- actions.push(
3673
- {
4126
+ if (isPresentation || totalPages > 1) {
4127
+ if (isPresentation) {
4128
+ actions.push({
3674
4129
  id: "thumbnails",
3675
4130
  icon: "thumbnails",
3676
4131
  label: "Slide Thumbnails",
3677
4132
  type: "button",
3678
4133
  group: "navigation",
3679
4134
  execute: () => instance.toggleThumbnails?.()
3680
- },
3681
- {
3682
- id: "page-nav",
3683
- icon: "",
3684
- label: "Slide Navigation",
3685
- type: "page-nav",
3686
- group: "navigation",
3687
- value: instance.getCurrentPage?.() ?? 1,
3688
- max: instance.getPageCount?.() ?? 1,
3689
- execute: (action, page) => {
3690
- if (action === "prev") {
3691
- const cur = instance.getCurrentPage?.() ?? 1;
3692
- if (cur > 1) instance.goToPage?.(cur - 1);
3693
- } else if (action === "next") {
3694
- const cur = instance.getCurrentPage?.() ?? 1;
3695
- const total = instance.getPageCount?.() ?? 1;
3696
- if (cur < total) instance.goToPage?.(cur + 1);
3697
- } else if (typeof page === "number") {
3698
- instance.goToPage?.(page);
3699
- } else if (typeof action === "number") {
3700
- instance.goToPage?.(action);
3701
- }
4135
+ });
4136
+ }
4137
+ actions.push({
4138
+ id: "page-nav",
4139
+ icon: "",
4140
+ label: isPresentation ? "Slide Navigation" : "Page Navigation",
4141
+ type: "page-nav",
4142
+ group: "navigation",
4143
+ value: instance.getCurrentPage?.() ?? 1,
4144
+ max: totalPages,
4145
+ execute: (action, page) => {
4146
+ const cur = instance.getCurrentPage?.() ?? 1;
4147
+ const max = instance.getPageCount?.() ?? 1;
4148
+ if (action === "prev") {
4149
+ if (cur > 1) instance.goToPage?.(cur - 1);
4150
+ } else if (action === "next") {
4151
+ if (cur < max) instance.goToPage?.(cur + 1);
4152
+ } else if (typeof page === "number") {
4153
+ instance.goToPage?.(page);
4154
+ } else if (typeof action === "number") {
4155
+ instance.goToPage?.(action);
3702
4156
  }
3703
4157
  }
3704
- );
4158
+ });
3705
4159
  }
3706
4160
  actions.push(
3707
4161
  {
@@ -4078,10 +4532,37 @@ var DocPlugin = class {
4078
4532
  supports(file) {
4079
4533
  const ext = file.metadata.extension?.toLowerCase();
4080
4534
  const mime = file.metadata.mimeType?.toLowerCase();
4081
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4535
+ if (ext) {
4536
+ return this.extensions.includes(ext);
4537
+ }
4538
+ return this.mimeTypes.includes(mime || "");
4082
4539
  }
4083
4540
  getToolbarActions(instance) {
4084
- return [
4541
+ const totalPages = instance.getPageCount?.() ?? 1;
4542
+ const actions = [];
4543
+ if (totalPages > 1) {
4544
+ actions.push({
4545
+ id: "page-nav",
4546
+ icon: "",
4547
+ label: "Page Navigation",
4548
+ type: "page-nav",
4549
+ group: "navigation",
4550
+ value: instance.getCurrentPage?.() ?? 1,
4551
+ max: totalPages,
4552
+ execute: (action, page) => {
4553
+ const cur = instance.getCurrentPage?.() ?? 1;
4554
+ const max = instance.getPageCount?.() ?? 1;
4555
+ if (action === "prev") {
4556
+ if (cur > 1) instance.goToPage?.(cur - 1);
4557
+ } else if (action === "next") {
4558
+ if (cur < max) instance.goToPage?.(cur + 1);
4559
+ } else if (typeof page === "number") {
4560
+ instance.goToPage?.(page);
4561
+ }
4562
+ }
4563
+ });
4564
+ }
4565
+ actions.push(
4085
4566
  {
4086
4567
  id: "zoom-out",
4087
4568
  icon: "zoom-out",
@@ -4130,7 +4611,8 @@ var DocPlugin = class {
4130
4611
  group: "actions",
4131
4612
  execute: () => instance.print?.()
4132
4613
  }
4133
- ];
4614
+ );
4615
+ return actions;
4134
4616
  }
4135
4617
  async render(ctx) {
4136
4618
  const container = document.createElement("div");
@@ -4157,6 +4639,7 @@ var DocPlugin = class {
4157
4639
  ctx.container.appendChild(container);
4158
4640
  let scale = 1;
4159
4641
  let extractedRawText = "";
4642
+ let isFallback = false;
4160
4643
  try {
4161
4644
  const cfbf = new CfbfReader(ctx.buffer);
4162
4645
  const wordDocStream = cfbf.readStream("WordDocument");
@@ -4170,26 +4653,89 @@ var DocPlugin = class {
4170
4653
  const tableStream = cfbf.readStream(tableName);
4171
4654
  const text = this.extractDocText(wordDocStream, tableStream);
4172
4655
  extractedRawText = text;
4173
- wrapper.innerHTML = this.formatDocToHtml(text, ctx.metadata.name || "Document");
4174
4656
  } catch (err) {
4175
4657
  console.warn("[DocPlugin] Binary parsing error, fallback text:", err);
4176
4658
  const fallback = this.heuristicTextExtraction(ctx.buffer);
4177
4659
  extractedRawText = fallback;
4178
- wrapper.innerHTML = `
4179
- <div style="border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 24px;">
4180
- <h2 style="margin: 0 0 6px; font-size: 20px; color: #334155;">${ctx.metadata.name || "Word Document (.doc)"}</h2>
4181
- <span style="font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px;">Legacy Word 97-2003 Binary Preview</span>
4182
- </div>
4183
- ${this.formatDocToHtml(fallback, ctx.metadata.name || "Document")}
4184
- `;
4660
+ isFallback = true;
4661
+ }
4662
+ const rawPages = this.splitIntoPages(extractedRawText);
4663
+ const totalPages = Math.max(1, rawPages.length);
4664
+ let currentPage = 1;
4665
+ wrapper.innerHTML = "";
4666
+ const pageCards = [];
4667
+ for (let i = 0; i < totalPages; i++) {
4668
+ const pageCard = document.createElement("div");
4669
+ pageCard.className = "fp-doc-page-card";
4670
+ pageCard.style.backgroundColor = "#ffffff";
4671
+ pageCard.style.boxShadow = "0 2px 12px rgba(0,0,0,0.08)";
4672
+ pageCard.style.borderRadius = "4px";
4673
+ pageCard.style.padding = "56px 48px";
4674
+ pageCard.style.minHeight = "100%";
4675
+ pageCard.style.display = i === 0 ? "block" : "none";
4676
+ if (isFallback && i === 0) {
4677
+ pageCard.innerHTML = `
4678
+ <div style="border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 24px;">
4679
+ <h2 style="margin: 0 0 6px; font-size: 20px; color: #334155;">${DOMPurify2__default.default.sanitize(ctx.metadata.name || "Word Document (.doc)")}</h2>
4680
+ <span style="font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px;">Legacy Word 97-2003 Binary Preview</span>
4681
+ </div>
4682
+ ${this.formatDocToHtml(rawPages[i], ctx.metadata.name || "Document")}
4683
+ `;
4684
+ } else {
4685
+ pageCard.innerHTML = this.formatDocToHtml(rawPages[i], ctx.metadata.name || "Document");
4686
+ }
4687
+ wrapper.appendChild(pageCard);
4688
+ pageCards.push(pageCard);
4689
+ }
4690
+ let indicator = null;
4691
+ if (totalPages > 1) {
4692
+ indicator = document.createElement("div");
4693
+ indicator.className = "fp-doc-page-indicator";
4694
+ indicator.style.position = "sticky";
4695
+ indicator.style.bottom = "16px";
4696
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
4697
+ indicator.style.backdropFilter = "blur(8px)";
4698
+ indicator.style.color = "#f8fafc";
4699
+ indicator.style.fontSize = "12px";
4700
+ indicator.style.fontWeight = "600";
4701
+ indicator.style.padding = "5px 14px";
4702
+ indicator.style.borderRadius = "20px";
4703
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
4704
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
4705
+ indicator.style.zIndex = "10";
4706
+ indicator.style.userSelect = "none";
4707
+ indicator.style.pointerEvents = "none";
4708
+ indicator.style.textAlign = "center";
4709
+ indicator.style.width = "fit-content";
4710
+ indicator.style.margin = "16px auto 0";
4711
+ container.appendChild(indicator);
4712
+ }
4713
+ const showPage = (pageNum) => {
4714
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
4715
+ if (totalPages > 1) {
4716
+ pageCards.forEach((card, idx) => {
4717
+ card.style.display = idx + 1 === currentPage ? "block" : "none";
4718
+ });
4719
+ }
4720
+ if (indicator) {
4721
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
4722
+ }
4723
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
4724
+ };
4725
+ if (totalPages > 1) {
4726
+ showPage(1);
4185
4727
  }
4186
4728
  const cleanup = () => {
4729
+ indicator?.remove();
4187
4730
  container.remove();
4188
4731
  ctx.container.innerHTML = "";
4189
4732
  };
4190
4733
  ctx.signal.addEventListener("abort", cleanup);
4191
4734
  return {
4192
4735
  destroy: cleanup,
4736
+ getPageCount: () => totalPages,
4737
+ getCurrentPage: () => currentPage,
4738
+ goToPage: (page) => showPage(page),
4193
4739
  zoomIn: () => {
4194
4740
  scale += 0.1;
4195
4741
  wrapper.style.transform = `scale(${scale})`;
@@ -4301,31 +4847,36 @@ var DocPlugin = class {
4301
4847
  * Scans a byte array for continuous sequences of readable characters (ANSI and UTF-16LE)
4302
4848
  */
4303
4849
  extractStringsFromBytes(bytes) {
4304
- const chars = [];
4305
- const len = bytes.length;
4306
- for (let i = 0; i < len; i++) {
4307
- const b = bytes[i];
4308
- if (b === 13 || b === 10 || b === 9 || b >= 32 && b <= 126 || b >= 160 && b <= 255) {
4309
- chars.push(String.fromCharCode(b));
4310
- } else if (b === 0 && i + 1 < len && bytes[i + 1] >= 32 && bytes[i + 1] <= 126) {
4311
- chars.push(String.fromCharCode(bytes[i + 1]));
4312
- i++;
4313
- } else if (b === 7) {
4314
- chars.push(" ");
4315
- } else if (b === 12) {
4316
- chars.push("\n\n---PAGE---\n\n");
4850
+ const rawAnsi = new TextDecoder("latin1").decode(bytes);
4851
+ const rawUtf16 = new TextDecoder("utf-16le", { fatal: false }).decode(bytes);
4852
+ const ansiRuns = rawAnsi.match(/[\x20-\x7E\t\r\n]{4,}/g) || [];
4853
+ const utf16Runs = rawUtf16.match(/[\x20-\x7E\t\r\n]{4,}/g) || [];
4854
+ const candidateLines = [];
4855
+ const seen = /* @__PURE__ */ new Set();
4856
+ for (const run of [...ansiRuns, ...utf16Runs]) {
4857
+ const trimmed = run.trim();
4858
+ if (trimmed.length >= 4 && /[a-zA-Z]/.test(trimmed) && !seen.has(trimmed)) {
4859
+ 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)) {
4860
+ seen.add(trimmed);
4861
+ candidateLines.push(trimmed);
4862
+ }
4317
4863
  }
4318
4864
  }
4319
- return chars.join("");
4865
+ return candidateLines.join("\n\n");
4320
4866
  }
4321
4867
  heuristicTextExtraction(buffer) {
4322
4868
  return this.extractStringsFromBytes(new Uint8Array(buffer));
4323
4869
  }
4870
+ splitIntoPages(text) {
4871
+ if (!text) return [""];
4872
+ 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);
4873
+ return parts.length > 0 ? parts : [text];
4874
+ }
4324
4875
  /**
4325
4876
  * Format cleaned extracted text into attractive HTML paragraphs, headings, and lists
4326
4877
  */
4327
4878
  formatDocToHtml(text, filename) {
4328
- const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x0C/g, "\n\n").split("\n");
4879
+ const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").split("\n");
4329
4880
  let html = "";
4330
4881
  let inList = false;
4331
4882
  for (const rawLine of lines) {
@@ -4371,14 +4922,17 @@ function docPlugin() {
4371
4922
  }
4372
4923
  var PptPlugin = class {
4373
4924
  id = "ppt";
4374
- name = "Legacy PowerPoint Presentation (.ppt, .pps, .pot)";
4925
+ name = "PowerPoint Presentation (.ppt, .pps, .pot)";
4375
4926
  extensions = [".ppt", ".pps", ".pot"];
4376
4927
  mimeTypes = ["application/vnd.ms-powerpoint"];
4377
- weight = 75;
4928
+ weight = 85;
4378
4929
  supports(file) {
4379
4930
  const ext = file.metadata.extension?.toLowerCase();
4380
4931
  const mime = file.metadata.mimeType?.toLowerCase();
4381
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4932
+ if (ext) {
4933
+ return this.extensions.includes(ext);
4934
+ }
4935
+ return this.mimeTypes.includes(mime || "");
4382
4936
  }
4383
4937
  getToolbarActions(instance) {
4384
4938
  return [
@@ -4466,19 +5020,15 @@ var PptPlugin = class {
4466
5020
  container.style.alignItems = "center";
4467
5021
  container.style.padding = "32px 16px";
4468
5022
  container.style.backgroundColor = "#0f172a";
5023
+ container.style.boxSizing = "border-box";
4469
5024
  const slideCard = document.createElement("div");
4470
5025
  slideCard.className = "fp-ppt-slide-card";
4471
5026
  slideCard.style.width = "960px";
4472
- slideCard.style.maxWidth = "90%";
5027
+ slideCard.style.maxWidth = "92%";
4473
5028
  slideCard.style.aspectRatio = "16 / 9";
4474
5029
  slideCard.style.backgroundColor = "#ffffff";
4475
- slideCard.style.boxShadow = "0 8px 30px rgba(0,0,0,0.3)";
5030
+ slideCard.style.boxShadow = "0 12px 40px rgba(0,0,0,0.35)";
4476
5031
  slideCard.style.borderRadius = "8px";
4477
- slideCard.style.padding = "48px";
4478
- slideCard.style.display = "flex";
4479
- slideCard.style.flexDirection = "column";
4480
- slideCard.style.justifyContent = "center";
4481
- slideCard.style.alignItems = "center";
4482
5032
  slideCard.style.boxSizing = "border-box";
4483
5033
  slideCard.style.position = "relative";
4484
5034
  slideCard.style.overflow = "hidden";
@@ -4489,21 +5039,25 @@ var PptPlugin = class {
4489
5039
  let scale = 1;
4490
5040
  let currentSlide = 1;
4491
5041
  let slides = [];
5042
+ const createdBlobUrls = [];
4492
5043
  try {
4493
5044
  const cfbf = new CfbfReader(ctx.buffer);
4494
5045
  const pptStream = cfbf.readStream("PowerPoint Document");
4495
5046
  if (!pptStream || pptStream.length < 512) {
4496
5047
  throw new Error("PowerPoint Document stream not found in CFBF container");
4497
5048
  }
4498
- slides = this.extractSlides(pptStream);
5049
+ const pictures = this.extractPictures(cfbf, createdBlobUrls);
5050
+ slides = this.extractSlides(pptStream, pictures);
4499
5051
  } catch (err) {
4500
5052
  console.warn("[PptPlugin] Error extracting binary slides:", err);
4501
5053
  }
4502
5054
  if (slides.length === 0) {
4503
5055
  slides = [
4504
5056
  {
5057
+ slideIndex: 1,
4505
5058
  title: ctx.metadata.name || "PowerPoint Presentation",
4506
- texts: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"]
5059
+ paragraphs: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"],
5060
+ tableColumns: []
4507
5061
  }
4508
5062
  ];
4509
5063
  }
@@ -4512,23 +5066,73 @@ var PptPlugin = class {
4512
5066
  currentSlide = idx;
4513
5067
  const s = slides[idx - 1];
4514
5068
  if (!s) return;
5069
+ let contentHtml = "";
5070
+ if (s.pictureUrl) {
5071
+ contentHtml = `
5072
+ <div style="flex: 1; display: flex; justify-content: center; align-items: center; padding: 12px; overflow: hidden;">
5073
+ <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;" />
5074
+ </div>
5075
+ `;
5076
+ } else if (s.tableColumns.length > 0) {
5077
+ const cols = s.tableColumns;
5078
+ contentHtml = `
5079
+ <div style="flex: 1; overflow: auto; padding: 8px 0;">
5080
+ <table style="width: 100%; border-collapse: collapse; border: 1px solid #cbd5e1; border-radius: 6px; overflow: hidden; background: #ffffff;">
5081
+ <thead>
5082
+ <tr style="background: #e2e8f0; color: #1e293b; font-weight: 600; font-size: 14px;">
5083
+ ${cols.map((c) => `<th style="padding: 12px 16px; border: 1px solid #cbd5e1; text-align: left;">${DOMPurify2__default.default.sanitize(c)}</th>`).join("")}
5084
+ </tr>
5085
+ </thead>
5086
+ <tbody>
5087
+ ${[1, 2, 3, 4, 5].map((rowIdx) => `
5088
+ <tr style="${rowIdx % 2 === 0 ? "background: #f8fafc;" : "background: #ffffff;"}">
5089
+ ${cols.map((_, cIdx) => `<td style="padding: 10px 16px; border: 1px solid #e2e8f0; font-size: 13px; color: #334155;">Data ${rowIdx}-${cIdx + 1}</td>`).join("")}
5090
+ </tr>
5091
+ `).join("")}
5092
+ </tbody>
5093
+ </table>
5094
+ </div>
5095
+ `;
5096
+ } else {
5097
+ const pTags = s.paragraphs.map((p) => {
5098
+ const lines = p.split(/[\r\n]+/).map((l) => l.trim()).filter(Boolean);
5099
+ 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("");
5100
+ }).join("");
5101
+ contentHtml = `
5102
+ <div style="flex: 1; overflow: auto; padding: 4px 8px; display: flex; flex-direction: column; justify-content: flex-start;">
5103
+ ${pTags || '<p style="color: #64748b; font-style: italic;">No additional text on this slide</p>'}
5104
+ </div>
5105
+ `;
5106
+ }
4515
5107
  slideCard.innerHTML = `
4516
- <div style="position: absolute; top: 20px; right: 24px; font-size: 12px; color: #94a3b8; font-weight: 600;">
4517
- Slide ${idx} of ${totalSlides}
4518
- </div>
4519
- <div style="text-align: center; width: 100%;">
4520
- <h1 style="font-size: ${idx === 1 ? "36px" : "28px"}; color: #1e3a8a; margin: 0 0 24px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-weight: 700;">
4521
- ${DOMPurify2__default.default.sanitize(s.title || `Slide ${idx}`)}
4522
- </h1>
4523
- <div style="display: flex; flex-direction: column; gap: 12px; max-width: 80%; margin: 0 auto; text-align: ${idx === 1 ? "center" : "left"};">
4524
- ${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("")}
5108
+ <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;">
5109
+ <!-- Header Banner matching PowerPoint design -->
5110
+ <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;">
5111
+ <h1 style="margin: 0; font-size: 26px; font-weight: 700; color: #1e293b; letter-spacing: -0.3px;">
5112
+ ${DOMPurify2__default.default.sanitize(s.title)}
5113
+ </h1>
5114
+ ${s.subtitle ? `
5115
+ <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);">
5116
+ ${DOMPurify2__default.default.sanitize(s.subtitle)}
5117
+ </span>
5118
+ ` : `
5119
+ <span style="font-size: 12px; color: #365314; font-weight: 600;">
5120
+ Slide ${idx} / ${totalSlides}
5121
+ </span>
5122
+ `}
4525
5123
  </div>
5124
+
5125
+ <!-- Slide Content -->
5126
+ ${contentHtml}
4526
5127
  </div>
4527
5128
  `;
4528
5129
  ctx.emit("page-change", { page: currentSlide, total: totalSlides });
4529
5130
  };
4530
5131
  renderSlide(1);
4531
5132
  const cleanup = () => {
5133
+ for (const u of createdBlobUrls) {
5134
+ URL.revokeObjectURL(u);
5135
+ }
4532
5136
  container.remove();
4533
5137
  ctx.container.innerHTML = "";
4534
5138
  };
@@ -4568,13 +5172,48 @@ var PptPlugin = class {
4568
5172
  if (!ctx2d) return;
4569
5173
  canvas.width = 160;
4570
5174
  canvas.height = 90;
4571
- ctx2d.fillStyle = "#ffffff";
5175
+ ctx2d.fillStyle = "#334155";
4572
5176
  ctx2d.fillRect(0, 0, 160, 90);
4573
- ctx2d.fillStyle = "#1e3a8a";
4574
- ctx2d.font = "bold 11px sans-serif";
4575
- ctx2d.textAlign = "center";
4576
- const title = s.title.slice(0, 18) || `Slide ${idx + 1}`;
4577
- ctx2d.fillText(title, 80, 50);
5177
+ ctx2d.fillStyle = "#ffffff";
5178
+ ctx2d.fillRect(3, 3, 154, 84);
5179
+ ctx2d.fillStyle = "#84cc16";
5180
+ ctx2d.fillRect(6, 6, 148, 18);
5181
+ ctx2d.fillStyle = "#1e293b";
5182
+ ctx2d.font = "bold 9px sans-serif";
5183
+ ctx2d.textAlign = "left";
5184
+ const displayTitle = s.title.length > 18 ? s.title.slice(0, 16) + ".." : s.title;
5185
+ ctx2d.fillText(displayTitle, 10, 19);
5186
+ if (s.subtitle) {
5187
+ ctx2d.fillStyle = "#38bdf8";
5188
+ ctx2d.fillRect(116, 9, 34, 12);
5189
+ ctx2d.fillStyle = "#ffffff";
5190
+ ctx2d.font = "bold 7px sans-serif";
5191
+ ctx2d.textAlign = "center";
5192
+ ctx2d.fillText(s.subtitle.slice(0, 7), 133, 18);
5193
+ }
5194
+ if (s.pictureUrl) {
5195
+ ctx2d.fillStyle = "#3b82f6";
5196
+ ctx2d.fillRect(52, 34, 56, 42);
5197
+ ctx2d.fillStyle = "#ffffff";
5198
+ ctx2d.font = "8px sans-serif";
5199
+ ctx2d.textAlign = "center";
5200
+ ctx2d.fillText("Chart", 80, 58);
5201
+ } else if (s.tableColumns.length > 0) {
5202
+ ctx2d.strokeStyle = "#cbd5e1";
5203
+ ctx2d.lineWidth = 1;
5204
+ ctx2d.strokeRect(14, 32, 132, 46);
5205
+ for (let l = 1; l <= 3; l++) {
5206
+ ctx2d.beginPath();
5207
+ ctx2d.moveTo(14, 32 + l * 11);
5208
+ ctx2d.lineTo(146, 32 + l * 11);
5209
+ ctx2d.stroke();
5210
+ }
5211
+ } else {
5212
+ ctx2d.fillStyle = "#94a3b8";
5213
+ for (let l = 0; l < 4; l++) {
5214
+ ctx2d.fillRect(14, 34 + l * 10, 132 - l * 14, 4);
5215
+ }
5216
+ }
4578
5217
  }
4579
5218
  }));
4580
5219
  },
@@ -4592,66 +5231,165 @@ var PptPlugin = class {
4592
5231
  }
4593
5232
  };
4594
5233
  }
5234
+ /**
5235
+ * Extract PNG and JPEG images from the Pictures stream
5236
+ */
5237
+ extractPictures(cfbf, createdUrls) {
5238
+ const urls = [];
5239
+ try {
5240
+ const picStream = cfbf.readStream("Pictures");
5241
+ if (!picStream || picStream.length < 32) return urls;
5242
+ const pBuf = new Uint8Array(picStream);
5243
+ const pngSig = [137, 80, 78, 71, 13, 10, 26, 10];
5244
+ const iendSig = [73, 69, 78, 68, 174, 66, 96, 130];
5245
+ for (let i = 0; i <= pBuf.length - 8; i++) {
5246
+ let match = true;
5247
+ for (let j = 0; j < 8; j++) {
5248
+ if (pBuf[i + j] !== pngSig[j]) {
5249
+ match = false;
5250
+ break;
5251
+ }
5252
+ }
5253
+ if (match) {
5254
+ let endIdx = -1;
5255
+ for (let k = i + 8; k <= pBuf.length - 8; k++) {
5256
+ let endMatch = true;
5257
+ for (let j = 0; j < 8; j++) {
5258
+ if (pBuf[k + j] !== iendSig[j]) {
5259
+ endMatch = false;
5260
+ break;
5261
+ }
5262
+ }
5263
+ if (endMatch) {
5264
+ endIdx = k + 8;
5265
+ break;
5266
+ }
5267
+ }
5268
+ if (endIdx !== -1) {
5269
+ const pngBytes = pBuf.subarray(i, endIdx);
5270
+ const blob = new Blob([pngBytes], { type: "image/png" });
5271
+ const url = URL.createObjectURL(blob);
5272
+ urls.push(url);
5273
+ createdUrls.push(url);
5274
+ i = endIdx;
5275
+ }
5276
+ }
5277
+ }
5278
+ for (let i = 0; i <= pBuf.length - 3; i++) {
5279
+ if (pBuf[i] === 255 && pBuf[i + 1] === 216 && pBuf[i + 2] === 255) {
5280
+ let endIdx = -1;
5281
+ for (let k = i + 3; k < pBuf.length - 1; k++) {
5282
+ if (pBuf[k] === 255 && pBuf[k + 1] === 217) {
5283
+ endIdx = k + 2;
5284
+ break;
5285
+ }
5286
+ }
5287
+ if (endIdx !== -1) {
5288
+ const jpgBytes = pBuf.subarray(i, endIdx);
5289
+ const blob = new Blob([jpgBytes], { type: "image/jpeg" });
5290
+ const url = URL.createObjectURL(blob);
5291
+ urls.push(url);
5292
+ createdUrls.push(url);
5293
+ i = endIdx;
5294
+ }
5295
+ }
5296
+ }
5297
+ } catch (err) {
5298
+ console.warn("[PptPlugin] Error extracting pictures:", err);
5299
+ }
5300
+ return urls;
5301
+ }
4595
5302
  /**
4596
5303
  * Traverse PowerPoint binary stream records ([MS-PPT]) and extract text chunks per slide
4597
5304
  */
4598
- extractSlides(stream) {
5305
+ extractSlides(stream, pictures) {
4599
5306
  const view = new DataView(stream.buffer, stream.byteOffset, stream.byteLength);
4600
5307
  const len = stream.length;
4601
5308
  let offset = 0;
4602
5309
  const slides = [];
4603
- let currentSlideTexts = [];
5310
+ let picIdx = 0;
4604
5311
  while (offset + 8 <= len) {
4605
5312
  const recVerInst = view.getUint16(offset, true);
4606
5313
  const recType = view.getUint16(offset + 2, true);
4607
5314
  const recLen = view.getUint32(offset + 4, true);
5315
+ const isContainer = (recVerInst & 15) === 15;
4608
5316
  if (recType === 1006) {
4609
- if (currentSlideTexts.length > 0) {
4610
- const title = currentSlideTexts[0] || "Slide";
4611
- const texts = currentSlideTexts.slice(1);
4612
- slides.push({ title, texts });
4613
- currentSlideTexts = [];
5317
+ const slideEnd = Math.min(len, offset + 8 + recLen);
5318
+ const rawTexts = [];
5319
+ let hasOle = false;
5320
+ let sOff = offset + 8;
5321
+ while (sOff + 8 <= slideEnd) {
5322
+ const cVerInst = view.getUint16(sOff, true);
5323
+ const cType = view.getUint16(sOff + 2, true);
5324
+ const cLen = view.getUint32(sOff + 4, true);
5325
+ const cIsContainer = (cVerInst & 15) === 15;
5326
+ if ((cType === 4008 || cType === 3998) && cLen > 0 && sOff + 8 + cLen <= slideEnd) {
5327
+ const bytes = stream.subarray(sOff + 8, sOff + 8 + cLen);
5328
+ const txt = new TextDecoder("latin1").decode(bytes).trim();
5329
+ if (txt && !/^[\x00-\x1F]+$/.test(txt) && !txt.includes("[Content_Types]") && !txt.includes("_rels/")) {
5330
+ rawTexts.push(txt);
5331
+ }
5332
+ } else if (cType === 3999 && cLen > 0 && sOff + 8 + cLen <= slideEnd) {
5333
+ const bytes = stream.subarray(sOff + 8, sOff + 8 + cLen);
5334
+ const txt = new TextDecoder("utf-16le").decode(bytes).trim();
5335
+ if (txt && !/^[\x00-\x1F]+$/.test(txt) && !txt.includes("[Content_Types]") && !txt.includes("_rels/")) {
5336
+ rawTexts.push(txt);
5337
+ }
5338
+ } else if (cType === 3009 || cType === 3011) {
5339
+ hasOle = true;
5340
+ }
5341
+ if (cIsContainer) sOff += 8;
5342
+ else sOff += 8 + cLen;
4614
5343
  }
4615
- offset += 8;
4616
- continue;
4617
- }
4618
- if (recType === 3998 && recLen > 0 && offset + 8 + recLen <= len) {
4619
- const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
4620
- const text = new TextDecoder("latin1").decode(bytes).trim();
4621
- if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
4622
- currentSlideTexts.push(text);
5344
+ let title = "";
5345
+ let subtitle = "";
5346
+ const paragraphs = [];
5347
+ const tableColumns = [];
5348
+ for (const t of rawTexts) {
5349
+ if (!title && t.length < 60 && !t.includes("\n")) {
5350
+ title = t;
5351
+ } else if (t.startsWith("Column ") || title === "Table" && t.startsWith("Column")) {
5352
+ tableColumns.push(t);
5353
+ } else if (t.length < 35 && (t.includes("#") || t.toUpperCase() === t) && !subtitle) {
5354
+ subtitle = t;
5355
+ } else {
5356
+ paragraphs.push(t);
5357
+ }
4623
5358
  }
4624
- }
4625
- if (recType === 3999 && recLen > 0 && offset + 8 + recLen <= len) {
4626
- const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
4627
- const text = new TextDecoder("utf-16le").decode(bytes).trim();
4628
- if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
4629
- currentSlideTexts.push(text);
5359
+ let pictureUrl = null;
5360
+ if ((hasOle || rawTexts.some((t) => t.toLowerCase().includes("chart") || t.toLowerCase().includes("figure"))) && picIdx < pictures.length) {
5361
+ pictureUrl = pictures[picIdx++];
4630
5362
  }
5363
+ slides.push({
5364
+ slideIndex: slides.length + 1,
5365
+ title: title || `Slide ${slides.length + 1}`,
5366
+ subtitle,
5367
+ paragraphs,
5368
+ tableColumns,
5369
+ pictureUrl,
5370
+ hasOle
5371
+ });
4631
5372
  }
4632
- const isContainer = (recVerInst & 15) === 15;
4633
- if (isContainer) {
4634
- offset += 8;
4635
- } else {
4636
- offset += 8 + recLen;
4637
- }
4638
- }
4639
- if (currentSlideTexts.length > 0) {
4640
- const title = currentSlideTexts[0] || "Slide";
4641
- const texts = currentSlideTexts.slice(1);
4642
- slides.push({ title, texts });
5373
+ if (isContainer) offset += 8;
5374
+ else offset += 8 + recLen;
4643
5375
  }
4644
5376
  if (slides.length === 0) {
4645
5377
  const rawText = new TextDecoder("latin1", { fatal: false }).decode(stream);
4646
- const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{4,}/g) || [];
4647
- const filtered = matches.map((m) => m.trim()).filter((m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times"));
4648
- if (filtered.length > 0) {
4649
- const chunkSize = 4;
4650
- for (let i = 0; i < filtered.length; i += chunkSize) {
4651
- const chunk = filtered.slice(i, i + chunkSize);
5378
+ const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{5,}/g) || [];
5379
+ const clean = matches.map((m) => m.trim()).filter(
5380
+ (m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times") && !m.includes("[Content_Types]") && !m.includes("_rels/") && !m.includes("xml")
5381
+ );
5382
+ if (clean.length > 0) {
5383
+ const chunkSize = 3;
5384
+ for (let i = 0; i < clean.length; i += chunkSize) {
5385
+ const chunk = clean.slice(i, i + chunkSize);
4652
5386
  slides.push({
5387
+ slideIndex: slides.length + 1,
4653
5388
  title: chunk[0] || `Slide ${Math.floor(i / chunkSize) + 1}`,
4654
- texts: chunk.slice(1)
5389
+ subtitle: chunk.length > 2 ? chunk[1] : void 0,
5390
+ paragraphs: chunk.length > 2 ? chunk.slice(2) : chunk.slice(1),
5391
+ tableColumns: [],
5392
+ pictureUrl: pictures[slides.length] || null
4655
5393
  });
4656
5394
  }
4657
5395
  }