@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/angular.cjs CHANGED
@@ -5,6 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true });
5
5
  var core = require('@angular/core');
6
6
  var common = require('@angular/common');
7
7
  var DOMPurify2 = require('dompurify');
8
+ var pdfjsLib = require('pdfjs-dist');
8
9
  var docx = require('docx-preview');
9
10
  var fflate = require('fflate');
10
11
  var XLSX = require('xlsx');
@@ -15,7 +16,7 @@ var THREE = require('three');
15
16
  var STLLoader_js = require('three/examples/jsm/loaders/STLLoader.js');
16
17
  var OBJLoader_js = require('three/examples/jsm/loaders/OBJLoader.js');
17
18
  var OrbitControls_js = require('three/examples/jsm/controls/OrbitControls.js');
18
- var rtf_js = require('rtf.js');
19
+ var RTFJS = require('rtf.js/dist/RTFJS.bundle.js');
19
20
 
20
21
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
21
22
 
@@ -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
  var __create = Object.create;
47
50
  var __defProp = Object.defineProperty;
@@ -344,6 +347,39 @@ function detectOoxmlType(buffer) {
344
347
  }
345
348
  return "application/zip";
346
349
  }
350
+ function detectCfbfType(buffer) {
351
+ const bytes = new Uint8Array(buffer);
352
+ const hasUtf16le = (str) => {
353
+ const target = new Uint8Array(str.length * 2);
354
+ for (let i = 0; i < str.length; i++) {
355
+ target[i * 2] = str.charCodeAt(i);
356
+ target[i * 2 + 1] = 0;
357
+ }
358
+ const targetLen = target.length;
359
+ const max = bytes.length - targetLen;
360
+ for (let i = 0; i <= max; i++) {
361
+ let match = true;
362
+ for (let j = 0; j < targetLen; j++) {
363
+ if (bytes[i + j] !== target[j]) {
364
+ match = false;
365
+ break;
366
+ }
367
+ }
368
+ if (match) return true;
369
+ }
370
+ return false;
371
+ };
372
+ if (hasUtf16le("PowerPoint Document")) {
373
+ return { mime: "application/vnd.ms-powerpoint", extension: ".ppt" };
374
+ }
375
+ if (hasUtf16le("WordDocument")) {
376
+ return { mime: "application/msword", extension: ".doc" };
377
+ }
378
+ if (hasUtf16le("Workbook") || hasUtf16le("Book")) {
379
+ return { mime: "application/vnd.ms-excel", extension: ".xls" };
380
+ }
381
+ return null;
382
+ }
347
383
  function extractExtension(nameOrUrl) {
348
384
  try {
349
385
  const url = new URL(nameOrUrl);
@@ -408,6 +444,18 @@ async function sourceToArrayBuffer(source, signal) {
408
444
  } else {
409
445
  metadata.mimeType = metadata.mimeType ?? magicMime;
410
446
  }
447
+ } else if (magicMime === "application/x-cfbf") {
448
+ if (metadata.extension) {
449
+ metadata.mimeType = mimeFromExtension(metadata.extension) ?? magicMime;
450
+ } else {
451
+ const cfbf = detectCfbfType(buffer);
452
+ if (cfbf) {
453
+ metadata.mimeType = cfbf.mime;
454
+ metadata.extension = cfbf.extension;
455
+ } else {
456
+ metadata.mimeType = magicMime;
457
+ }
458
+ }
411
459
  } else {
412
460
  metadata.mimeType = magicMime;
413
461
  }
@@ -512,11 +560,21 @@ var ToolbarController = class {
512
560
  el;
513
561
  toolbarEl;
514
562
  actions = [];
563
+ pageInputEl = null;
564
+ pageLabelEl = null;
515
565
  constructor(container) {
516
566
  this.el = container;
517
567
  this.toolbarEl = createElement("div", { className: "fp-toolbar" });
518
568
  this.el.appendChild(this.toolbarEl);
519
569
  }
570
+ setPage(page, max) {
571
+ if (this.pageInputEl) {
572
+ this.pageInputEl.value = page.toString();
573
+ }
574
+ if (max !== void 0 && this.pageLabelEl) {
575
+ this.pageLabelEl.textContent = ` / ${max}`;
576
+ }
577
+ }
520
578
  update(actions) {
521
579
  this.actions = actions;
522
580
  this.render();
@@ -559,6 +617,7 @@ var ToolbarController = class {
559
617
  if (action.type === "separator") {
560
618
  groupEl.appendChild(createElement("div", { className: "fp-toolbar-separator" }));
561
619
  } else if (action.type === "page-nav") {
620
+ const max = action.max ?? 1;
562
621
  const prevBtn = this.createButton(
563
622
  "prev",
564
623
  ICON_PAGE_PREV,
@@ -577,7 +636,6 @@ var ToolbarController = class {
577
636
  "Next Page",
578
637
  () => {
579
638
  const cur = parseInt(input.value, 10) || 1;
580
- const max = action.max ?? 1;
581
639
  if (cur < max) {
582
640
  input.value = (cur + 1).toString();
583
641
  action.execute("next", cur + 1);
@@ -589,15 +647,18 @@ var ToolbarController = class {
589
647
  type: "number",
590
648
  value: (action.value ?? 1).toString(),
591
649
  min: "1",
592
- max: (action.max ?? 1).toString()
650
+ max: max.toString()
593
651
  });
594
652
  input.addEventListener("change", () => {
595
- const val = parseInt(input.value, 10);
596
- if (!isNaN(val)) {
597
- action.execute("go", val);
598
- }
653
+ let val = parseInt(input.value, 10);
654
+ if (isNaN(val)) val = 1;
655
+ val = Math.max(1, Math.min(max, val));
656
+ input.value = val.toString();
657
+ action.execute("go", val);
599
658
  });
600
- const label = createElement("span", { className: "fp-toolbar-label" }, ` / ${action.max ?? 1}`);
659
+ const label = createElement("span", { className: "fp-toolbar-label" }, ` / ${max}`);
660
+ this.pageInputEl = input;
661
+ this.pageLabelEl = label;
601
662
  groupEl.appendChild(prevBtn);
602
663
  groupEl.appendChild(input);
603
664
  groupEl.appendChild(label);
@@ -775,7 +836,13 @@ var FilePreviewViewer = class {
775
836
  buffer,
776
837
  options,
777
838
  signal,
778
- emit: (event, payload) => this.eventEmitter.emit(event, payload)
839
+ emit: (event, payload) => {
840
+ if (event === "page-change" && payload && typeof payload.page === "number") {
841
+ const total = payload.total ?? payload.totalPages;
842
+ this.toolbar?.setPage(payload.page, total);
843
+ }
844
+ this.eventEmitter.emit(event, payload);
845
+ }
779
846
  });
780
847
  this.activeInstance = instance;
781
848
  this.hideLoading();
@@ -809,6 +876,7 @@ var FilePreviewViewer = class {
809
876
  if (thumbnails && thumbnails.length > 0 && this.thumbnailPanel) {
810
877
  this.thumbnailPanel.update(thumbnails, (index) => {
811
878
  instance.goToPage?.(index + 1);
879
+ this.toolbar?.setPage(index + 1);
812
880
  });
813
881
  if (options.showThumbnails) {
814
882
  this.thumbnailPanel.show();
@@ -934,9 +1002,13 @@ var FilePreviewViewer = class {
934
1002
  if (e.key === "ArrowRight" || e.key === "PageDown") {
935
1003
  const cur = this.activeInstance.getCurrentPage?.() ?? 1;
936
1004
  this.activeInstance.goToPage?.(cur + 1);
1005
+ const nextCur = this.activeInstance.getCurrentPage?.() ?? cur + 1;
1006
+ this.toolbar?.setPage(nextCur);
937
1007
  } else if (e.key === "ArrowLeft" || e.key === "PageUp") {
938
1008
  const cur = this.activeInstance.getCurrentPage?.() ?? 1;
939
1009
  this.activeInstance.goToPage?.(Math.max(1, cur - 1));
1010
+ const prevCur = this.activeInstance.getCurrentPage?.() ?? Math.max(1, cur - 1);
1011
+ this.toolbar?.setPage(prevCur);
940
1012
  } else if (e.key === "+" || e.key === "=") {
941
1013
  this.activeInstance.zoomIn?.();
942
1014
  } else if (e.key === "-" || e.key === "_") {
@@ -1202,30 +1274,62 @@ var CfbfReader = class {
1202
1274
  return result.subarray(0, targetSize);
1203
1275
  }
1204
1276
  };
1205
-
1206
- // ../plugins/pdf/dist/index.js
1277
+ if (typeof window !== "undefined" && pdfjsLib__namespace.GlobalWorkerOptions) {
1278
+ if (!pdfjsLib__namespace.GlobalWorkerOptions.workerSrc) {
1279
+ pdfjsLib__namespace.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib__namespace.version || "4.10.38"}/build/pdf.worker.min.mjs`;
1280
+ }
1281
+ }
1207
1282
  var PdfPlugin = class {
1208
1283
  id = "pdf";
1209
- name = "PDF Preview";
1284
+ name = "PDF Document Preview";
1210
1285
  extensions = [".pdf"];
1211
1286
  mimeTypes = ["application/pdf"];
1212
1287
  weight = 100;
1213
1288
  supports(file) {
1214
1289
  const ext = file.metadata.extension?.toLowerCase();
1215
1290
  const mime = file.metadata.mimeType?.toLowerCase();
1216
- return ext === ".pdf" || mime === "application/pdf";
1291
+ if (ext) return this.extensions.includes(ext);
1292
+ return this.mimeTypes.includes(mime || "");
1217
1293
  }
1218
1294
  getToolbarActions(instance) {
1295
+ const totalPages = instance.getPageCount?.() ?? 1;
1296
+ const curPage = instance.getCurrentPage?.() ?? 1;
1219
1297
  return [
1298
+ {
1299
+ id: "thumbnails",
1300
+ icon: "thumbnails",
1301
+ label: "Page Thumbnails",
1302
+ type: "button",
1303
+ group: "navigation",
1304
+ execute: () => instance.toggleThumbnails?.()
1305
+ },
1306
+ {
1307
+ id: "page-nav",
1308
+ icon: "",
1309
+ label: "Page Navigation",
1310
+ type: "page-nav",
1311
+ group: "navigation",
1312
+ value: curPage,
1313
+ max: totalPages,
1314
+ execute: (action, page) => {
1315
+ const cur = instance.getCurrentPage?.() ?? 1;
1316
+ const max = instance.getPageCount?.() ?? 1;
1317
+ if (action === "prev") {
1318
+ if (cur > 1) instance.goToPage?.(cur - 1);
1319
+ } else if (action === "next") {
1320
+ if (cur < max) instance.goToPage?.(cur + 1);
1321
+ } else if (typeof page === "number") {
1322
+ instance.goToPage?.(page);
1323
+ }
1324
+ }
1325
+ },
1220
1326
  {
1221
1327
  id: "zoom-out",
1222
1328
  icon: "zoom-out",
1223
1329
  label: "Zoom Out",
1224
1330
  type: "button",
1225
1331
  group: "zoom",
1226
- execute: () => {
1227
- instance.zoomOut?.();
1228
- }
1332
+ execute: () => instance.zoomOut?.()
1229
1333
  },
1230
1334
  {
1231
1335
  id: "zoom-in",
@@ -1233,9 +1337,7 @@ var PdfPlugin = class {
1233
1337
  label: "Zoom In",
1234
1338
  type: "button",
1235
1339
  group: "zoom",
1236
- execute: () => {
1237
- instance.zoomIn?.();
1238
- }
1340
+ execute: () => instance.zoomIn?.()
1239
1341
  },
1240
1342
  {
1241
1343
  id: "fit-page",
@@ -1243,39 +1345,23 @@ var PdfPlugin = class {
1243
1345
  label: "Fit to Page",
1244
1346
  type: "button",
1245
1347
  group: "zoom",
1246
- execute: () => {
1247
- instance.fitToPage?.();
1248
- }
1348
+ execute: () => instance.fitToPage?.()
1249
1349
  },
1250
1350
  {
1251
1351
  id: "rotate-cw",
1252
1352
  icon: "rotate-cw",
1253
- label: "Rotate",
1353
+ label: "Rotate Clockwise",
1254
1354
  type: "button",
1255
1355
  group: "view",
1256
- execute: () => {
1257
- instance.rotateCW?.();
1258
- }
1259
- },
1260
- {
1261
- id: "page-nav",
1262
- icon: "page-nav",
1263
- label: "Page Navigation",
1264
- type: "page-nav",
1265
- group: "navigation",
1266
- execute: (page) => {
1267
- if (typeof page === "number") instance.goToPage?.(page);
1268
- }
1356
+ execute: () => instance.rotateCW?.()
1269
1357
  },
1270
1358
  {
1271
1359
  id: "download",
1272
1360
  icon: "download",
1273
- label: "Download",
1361
+ label: "Download PDF",
1274
1362
  type: "button",
1275
1363
  group: "actions",
1276
- execute: () => {
1277
- instance.download?.();
1278
- }
1364
+ execute: () => instance.download?.()
1279
1365
  },
1280
1366
  {
1281
1367
  id: "print",
@@ -1283,99 +1369,213 @@ var PdfPlugin = class {
1283
1369
  label: "Print",
1284
1370
  type: "button",
1285
1371
  group: "actions",
1286
- execute: () => {
1287
- instance.print?.();
1288
- }
1372
+ execute: () => instance.print?.()
1289
1373
  }
1290
1374
  ];
1291
1375
  }
1292
1376
  async render(ctx) {
1293
- const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1294
- const url = URL.createObjectURL(blob);
1295
- const wrapper = document.createElement("div");
1296
- wrapper.style.width = "100%";
1297
- wrapper.style.height = "100%";
1298
- wrapper.style.overflow = "hidden";
1299
- wrapper.style.display = "flex";
1300
- wrapper.style.justifyContent = "center";
1301
- wrapper.style.alignItems = "center";
1302
- const iframe = document.createElement("iframe");
1303
- iframe.src = url;
1304
- iframe.style.width = "100%";
1305
- iframe.style.height = "100%";
1306
- iframe.style.border = "none";
1307
- wrapper.appendChild(iframe);
1308
- ctx.container.appendChild(wrapper);
1377
+ const container = document.createElement("div");
1378
+ container.className = "fp-pdf-container";
1379
+ container.style.width = "100%";
1380
+ container.style.height = "100%";
1381
+ container.style.overflow = "auto";
1382
+ container.style.display = "flex";
1383
+ container.style.flexDirection = "column";
1384
+ container.style.alignItems = "center";
1385
+ container.style.padding = "24px 16px";
1386
+ container.style.backgroundColor = "#0f172a";
1387
+ container.style.boxSizing = "border-box";
1388
+ container.style.position = "relative";
1389
+ const pageCard = document.createElement("div");
1390
+ pageCard.className = "fp-pdf-page-card";
1391
+ pageCard.style.boxShadow = "0 10px 35px rgba(0, 0, 0, 0.5)";
1392
+ pageCard.style.backgroundColor = "#ffffff";
1393
+ pageCard.style.borderRadius = "4px";
1394
+ pageCard.style.overflow = "hidden";
1395
+ pageCard.style.lineHeight = "0";
1396
+ pageCard.style.transition = "transform 0.15s ease";
1397
+ pageCard.style.position = "relative";
1398
+ const canvas = document.createElement("canvas");
1399
+ pageCard.appendChild(canvas);
1400
+ container.appendChild(pageCard);
1401
+ const indicator = document.createElement("div");
1402
+ indicator.className = "fp-pdf-page-indicator";
1403
+ indicator.style.position = "sticky";
1404
+ indicator.style.bottom = "16px";
1405
+ indicator.style.marginTop = "16px";
1406
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
1407
+ indicator.style.backdropFilter = "blur(8px)";
1408
+ indicator.style.color = "#f8fafc";
1409
+ indicator.style.fontSize = "12px";
1410
+ indicator.style.fontWeight = "600";
1411
+ indicator.style.padding = "5px 14px";
1412
+ indicator.style.borderRadius = "20px";
1413
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
1414
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
1415
+ indicator.style.zIndex = "10";
1416
+ indicator.style.userSelect = "none";
1417
+ indicator.style.pointerEvents = "none";
1418
+ container.appendChild(indicator);
1419
+ ctx.container.appendChild(container);
1420
+ const loadingTask = pdfjsLib__namespace.getDocument({
1421
+ data: new Uint8Array(ctx.buffer),
1422
+ cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib__namespace.version || "4.10.38"}/cmaps/`,
1423
+ cMapPacked: true,
1424
+ standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib__namespace.version || "4.10.38"}/standard_fonts/`
1425
+ });
1426
+ const pdfDoc = await loadingTask.promise;
1427
+ const totalPages = Math.max(1, pdfDoc.numPages);
1309
1428
  let currentPage = 1;
1310
- let currentZoom = 1;
1429
+ let zoomScale = 1;
1311
1430
  let rotation = 0;
1431
+ let currentRenderTask = null;
1432
+ const renderPage = async (pageNum) => {
1433
+ if (currentRenderTask) {
1434
+ try {
1435
+ currentRenderTask.cancel();
1436
+ } catch {
1437
+ }
1438
+ currentRenderTask = null;
1439
+ }
1440
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
1441
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
1442
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
1443
+ const page = await pdfDoc.getPage(currentPage);
1444
+ const containerWidth = container.clientWidth || 900;
1445
+ const unscaledVp = page.getViewport({ scale: 1, rotation });
1446
+ const baseScale = Math.min((containerWidth - 64) / unscaledVp.width, 1.6);
1447
+ const effectiveScale = (baseScale > 0 ? baseScale : 1) * zoomScale;
1448
+ const pixelRatio = window.devicePixelRatio || 1;
1449
+ const viewport = page.getViewport({ scale: effectiveScale, rotation });
1450
+ canvas.width = Math.floor(viewport.width * pixelRatio);
1451
+ canvas.height = Math.floor(viewport.height * pixelRatio);
1452
+ canvas.style.width = `${Math.floor(viewport.width)}px`;
1453
+ canvas.style.height = `${Math.floor(viewport.height)}px`;
1454
+ const canvasCtx = canvas.getContext("2d");
1455
+ if (!canvasCtx) return;
1456
+ canvasCtx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
1457
+ currentRenderTask = page.render({
1458
+ canvasContext: canvasCtx,
1459
+ viewport
1460
+ });
1461
+ try {
1462
+ await currentRenderTask.promise;
1463
+ } catch (err) {
1464
+ if (err?.name !== "RenderingCancelledException") {
1465
+ console.warn("[PdfPlugin] Page render warning:", err);
1466
+ }
1467
+ } finally {
1468
+ currentRenderTask = null;
1469
+ }
1470
+ };
1471
+ await renderPage(1);
1312
1472
  const cleanup = () => {
1313
- URL.revokeObjectURL(url);
1314
- wrapper.remove();
1473
+ if (currentRenderTask) {
1474
+ try {
1475
+ currentRenderTask.cancel();
1476
+ } catch {
1477
+ }
1478
+ }
1479
+ try {
1480
+ pdfDoc.destroy();
1481
+ } catch {
1482
+ }
1483
+ container.remove();
1315
1484
  ctx.container.innerHTML = "";
1316
1485
  };
1317
1486
  ctx.signal.addEventListener("abort", cleanup);
1318
- return {
1487
+ const instance = {
1319
1488
  destroy: cleanup,
1320
1489
  zoomIn: () => {
1321
- currentZoom += 0.1;
1322
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1490
+ zoomScale = Math.min(3.5, zoomScale + 0.2);
1491
+ renderPage(currentPage);
1323
1492
  },
1324
1493
  zoomOut: () => {
1325
- currentZoom = Math.max(0.2, currentZoom - 0.1);
1326
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1494
+ zoomScale = Math.max(0.3, zoomScale - 0.2);
1495
+ renderPage(currentPage);
1327
1496
  },
1328
- getZoom: () => currentZoom,
1497
+ getZoom: () => zoomScale,
1329
1498
  setZoom: (level) => {
1330
- currentZoom = level;
1331
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1499
+ zoomScale = Math.max(0.3, Math.min(3.5, level));
1500
+ renderPage(currentPage);
1332
1501
  },
1333
1502
  fitToPage: () => {
1334
- currentZoom = 1;
1335
- iframe.style.transform = `scale(1) rotate(${rotation}deg)`;
1503
+ zoomScale = 1;
1504
+ renderPage(currentPage);
1336
1505
  },
1337
1506
  rotateCW: () => {
1338
1507
  rotation = (rotation + 90) % 360;
1339
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1508
+ renderPage(currentPage);
1340
1509
  },
1341
1510
  rotateCCW: () => {
1342
1511
  rotation = (rotation - 90 + 360) % 360;
1343
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1512
+ renderPage(currentPage);
1344
1513
  },
1345
1514
  getRotation: () => rotation,
1515
+ getPageCount: () => totalPages,
1516
+ getCurrentPage: () => currentPage,
1346
1517
  goToPage: (page) => {
1347
- currentPage = page;
1348
- iframe.src = `${url}#page=${page}`;
1518
+ renderPage(page);
1349
1519
  },
1350
- getCurrentPage: () => currentPage,
1351
1520
  download: () => {
1521
+ const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1522
+ const url = URL.createObjectURL(blob);
1352
1523
  const a = document.createElement("a");
1353
1524
  a.href = url;
1354
1525
  a.download = ctx.metadata.name || "document.pdf";
1355
1526
  a.click();
1527
+ URL.revokeObjectURL(url);
1356
1528
  },
1357
1529
  print: () => {
1358
- iframe.contentWindow?.print();
1530
+ const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1531
+ const url = URL.createObjectURL(blob);
1532
+ const hiddenIframe = document.createElement("iframe");
1533
+ hiddenIframe.style.position = "fixed";
1534
+ hiddenIframe.style.right = "0";
1535
+ hiddenIframe.style.bottom = "0";
1536
+ hiddenIframe.style.width = "0";
1537
+ hiddenIframe.style.height = "0";
1538
+ hiddenIframe.style.border = "0";
1539
+ document.body.appendChild(hiddenIframe);
1540
+ hiddenIframe.src = url;
1541
+ hiddenIframe.onload = () => {
1542
+ setTimeout(() => {
1543
+ hiddenIframe.contentWindow?.print();
1544
+ setTimeout(() => {
1545
+ hiddenIframe.remove();
1546
+ URL.revokeObjectURL(url);
1547
+ }, 1e3);
1548
+ }, 300);
1549
+ };
1359
1550
  },
1360
1551
  getThumbnails: async () => {
1361
- return [
1362
- {
1363
- index: 1,
1364
- label: "Page 1",
1365
- render: async (canvas) => {
1366
- const context = canvas.getContext("2d");
1367
- if (context) {
1368
- context.fillStyle = "#fff";
1369
- context.fillRect(0, 0, canvas.width, canvas.height);
1370
- context.fillStyle = "#333";
1371
- context.font = "12px sans-serif";
1372
- context.fillText("PDF Preview", 10, 20);
1552
+ const thumbnails = [];
1553
+ const count = Math.min(totalPages, 50);
1554
+ for (let i = 1; i <= count; i++) {
1555
+ thumbnails.push({
1556
+ index: i,
1557
+ label: `Page ${i}`,
1558
+ render: async (thumbCanvas) => {
1559
+ try {
1560
+ const p = await pdfDoc.getPage(i);
1561
+ const baseVp = p.getViewport({ scale: 1 });
1562
+ const thumbScale = (thumbCanvas.width || 120) / baseVp.width;
1563
+ const thumbVp = p.getViewport({ scale: thumbScale });
1564
+ thumbCanvas.height = Math.floor(thumbVp.height);
1565
+ const tCtx = thumbCanvas.getContext("2d");
1566
+ if (tCtx) {
1567
+ await p.render({ canvasContext: tCtx, viewport: thumbVp }).promise;
1568
+ }
1569
+ } catch (e) {
1570
+ console.warn(`[PdfPlugin] Error generating thumbnail for page ${i}:`, e);
1373
1571
  }
1374
1572
  }
1375
- }
1376
- ];
1573
+ });
1574
+ }
1575
+ return thumbnails;
1377
1576
  }
1378
1577
  };
1578
+ return instance;
1379
1579
  }
1380
1580
  };
1381
1581
  function pdfPlugin() {
@@ -1696,10 +1896,37 @@ var DocxPlugin = class {
1696
1896
  supports(file) {
1697
1897
  const ext = file.metadata.extension?.toLowerCase();
1698
1898
  const mime = file.metadata.mimeType?.toLowerCase();
1699
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
1899
+ if (ext) {
1900
+ return this.extensions.includes(ext);
1901
+ }
1902
+ return this.mimeTypes.includes(mime || "");
1700
1903
  }
1701
1904
  getToolbarActions(instance) {
1702
- return [
1905
+ const totalPages = instance.getPageCount?.() ?? 1;
1906
+ const actions = [];
1907
+ if (totalPages > 1) {
1908
+ actions.push({
1909
+ id: "page-nav",
1910
+ icon: "",
1911
+ label: "Page Navigation",
1912
+ type: "page-nav",
1913
+ group: "navigation",
1914
+ value: instance.getCurrentPage?.() ?? 1,
1915
+ max: totalPages,
1916
+ execute: (action, page) => {
1917
+ const cur = instance.getCurrentPage?.() ?? 1;
1918
+ const max = instance.getPageCount?.() ?? 1;
1919
+ if (action === "prev") {
1920
+ if (cur > 1) instance.goToPage?.(cur - 1);
1921
+ } else if (action === "next") {
1922
+ if (cur < max) instance.goToPage?.(cur + 1);
1923
+ } else if (typeof page === "number") {
1924
+ instance.goToPage?.(page);
1925
+ }
1926
+ }
1927
+ });
1928
+ }
1929
+ actions.push(
1703
1930
  {
1704
1931
  id: "zoom-out",
1705
1932
  icon: "zoom-out",
@@ -1740,7 +1967,8 @@ var DocxPlugin = class {
1740
1967
  group: "actions",
1741
1968
  execute: () => instance.print?.()
1742
1969
  }
1743
- ];
1970
+ );
1971
+ return actions;
1744
1972
  }
1745
1973
  async render(ctx) {
1746
1974
  const wrapper = document.createElement("div");
@@ -1816,7 +2044,51 @@ var DocxPlugin = class {
1816
2044
  }
1817
2045
  }
1818
2046
  }
2047
+ const sections = wrapper.querySelectorAll("section.docx");
2048
+ const cards = wrapper.querySelectorAll(".fp-docx-page-card");
2049
+ const pageElements = sections.length > 0 ? sections : cards;
2050
+ const totalPages = Math.max(1, pageElements.length);
2051
+ let currentPage = 1;
2052
+ let indicator = null;
2053
+ if (totalPages > 1) {
2054
+ indicator = document.createElement("div");
2055
+ indicator.className = "fp-docx-page-indicator";
2056
+ indicator.style.position = "sticky";
2057
+ indicator.style.bottom = "16px";
2058
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
2059
+ indicator.style.backdropFilter = "blur(8px)";
2060
+ indicator.style.color = "#f8fafc";
2061
+ indicator.style.fontSize = "12px";
2062
+ indicator.style.fontWeight = "600";
2063
+ indicator.style.padding = "5px 14px";
2064
+ indicator.style.borderRadius = "20px";
2065
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
2066
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
2067
+ indicator.style.zIndex = "10";
2068
+ indicator.style.userSelect = "none";
2069
+ indicator.style.pointerEvents = "none";
2070
+ indicator.style.textAlign = "center";
2071
+ indicator.style.width = "fit-content";
2072
+ indicator.style.margin = "16px auto 0";
2073
+ ctx.container.appendChild(indicator);
2074
+ }
2075
+ const showPage = (pageNum) => {
2076
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
2077
+ if (pageElements.length > 1) {
2078
+ pageElements.forEach((sec, idx) => {
2079
+ sec.style.display = idx + 1 === currentPage ? "block" : "none";
2080
+ });
2081
+ }
2082
+ if (indicator) {
2083
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
2084
+ }
2085
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
2086
+ };
2087
+ if (totalPages > 1) {
2088
+ showPage(1);
2089
+ }
1819
2090
  const cleanup = () => {
2091
+ indicator?.remove();
1820
2092
  for (const url of createdBlobUrls) {
1821
2093
  URL.revokeObjectURL(url);
1822
2094
  }
@@ -1827,6 +2099,11 @@ var DocxPlugin = class {
1827
2099
  ctx.signal.addEventListener("abort", cleanup);
1828
2100
  return {
1829
2101
  destroy: cleanup,
2102
+ getPageCount: () => totalPages,
2103
+ getCurrentPage: () => currentPage,
2104
+ goToPage: (page) => {
2105
+ showPage(page);
2106
+ },
1830
2107
  zoomIn: () => {
1831
2108
  scale += 0.1;
1832
2109
  wrapper.style.transform = `scale(${scale})`;
@@ -2085,10 +2362,37 @@ var ExcelPlugin = class {
2085
2362
  supports(file) {
2086
2363
  const ext = file.metadata.extension?.toLowerCase();
2087
2364
  const mime = file.metadata.mimeType?.toLowerCase();
2088
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
2365
+ if (ext) {
2366
+ return this.extensions.includes(ext);
2367
+ }
2368
+ return this.mimeTypes.includes(mime || "");
2089
2369
  }
2090
2370
  getToolbarActions(instance) {
2091
- return [
2371
+ const totalSheets = instance.getPageCount?.() ?? 1;
2372
+ const actions = [];
2373
+ if (totalSheets > 1) {
2374
+ actions.push({
2375
+ id: "page-nav",
2376
+ icon: "",
2377
+ label: "Sheet Navigation",
2378
+ type: "page-nav",
2379
+ group: "navigation",
2380
+ value: instance.getCurrentPage?.() ?? 1,
2381
+ max: totalSheets,
2382
+ execute: (action, sheet) => {
2383
+ const cur = instance.getCurrentPage?.() ?? 1;
2384
+ const max = instance.getPageCount?.() ?? 1;
2385
+ if (action === "prev") {
2386
+ if (cur > 1) instance.goToPage?.(cur - 1);
2387
+ } else if (action === "next") {
2388
+ if (cur < max) instance.goToPage?.(cur + 1);
2389
+ } else if (typeof sheet === "number") {
2390
+ instance.goToPage?.(sheet);
2391
+ }
2392
+ }
2393
+ });
2394
+ }
2395
+ actions.push(
2092
2396
  {
2093
2397
  id: "zoom-out",
2094
2398
  icon: "zoom-out",
@@ -2109,16 +2413,6 @@ var ExcelPlugin = class {
2109
2413
  instance.zoomIn?.();
2110
2414
  }
2111
2415
  },
2112
- {
2113
- id: "page-nav",
2114
- icon: "page-nav",
2115
- label: "Sheet Navigation",
2116
- type: "page-nav",
2117
- group: "navigation",
2118
- execute: (sheet) => {
2119
- if (typeof sheet === "number") instance.goToPage?.(sheet);
2120
- }
2121
- },
2122
2416
  {
2123
2417
  id: "download",
2124
2418
  icon: "download",
@@ -2139,7 +2433,8 @@ var ExcelPlugin = class {
2139
2433
  instance.print?.();
2140
2434
  }
2141
2435
  }
2142
- ];
2436
+ );
2437
+ return actions;
2143
2438
  }
2144
2439
  async render(ctx) {
2145
2440
  const container = document.createElement("div");
@@ -2223,6 +2518,7 @@ var ExcelPlugin = class {
2223
2518
  b.style.fontWeight = "normal";
2224
2519
  }
2225
2520
  });
2521
+ ctx.emit("page-change", { page: currentSheetIndex, total: sheetNames.length });
2226
2522
  };
2227
2523
  if (sheetNames.length > 0) {
2228
2524
  sheetNames.forEach((name, idx) => {
@@ -2472,7 +2768,31 @@ var CodePlugin = class {
2472
2768
  return false;
2473
2769
  }
2474
2770
  getToolbarActions(instance) {
2475
- return [
2771
+ const totalPages = instance.getPageCount?.() ?? 1;
2772
+ const actions = [];
2773
+ if (totalPages > 1) {
2774
+ actions.push({
2775
+ id: "page-nav",
2776
+ icon: "",
2777
+ label: "Page Navigation",
2778
+ type: "page-nav",
2779
+ group: "navigation",
2780
+ value: instance.getCurrentPage?.() ?? 1,
2781
+ max: totalPages,
2782
+ execute: (action, page) => {
2783
+ const cur = instance.getCurrentPage?.() ?? 1;
2784
+ const max = instance.getPageCount?.() ?? 1;
2785
+ if (action === "prev") {
2786
+ if (cur > 1) instance.goToPage?.(cur - 1);
2787
+ } else if (action === "next") {
2788
+ if (cur < max) instance.goToPage?.(cur + 1);
2789
+ } else if (typeof page === "number") {
2790
+ instance.goToPage?.(page);
2791
+ }
2792
+ }
2793
+ });
2794
+ }
2795
+ actions.push(
2476
2796
  {
2477
2797
  id: "zoom-out",
2478
2798
  icon: "zoom-out",
@@ -2523,11 +2843,16 @@ var CodePlugin = class {
2523
2843
  instance.print?.();
2524
2844
  }
2525
2845
  }
2526
- ];
2846
+ );
2847
+ return actions;
2527
2848
  }
2528
2849
  async render(ctx) {
2529
2850
  const decoder = new TextDecoder("utf-8");
2530
- const text = decoder.decode(ctx.buffer);
2851
+ const fullText = decoder.decode(ctx.buffer);
2852
+ const pageSplitRegex = /(?:\f|\x0C|(?:\r?\n|^)\s*[-=_]{3,}\s*(?:PAGE|Page|page break|Page Break)[\s\d\w-]*[-=_]{3,}\s*(?:\r?\n|$))/i;
2853
+ const rawPages = fullText.split(pageSplitRegex).map((p) => p.trim()).filter((p) => p.length > 0);
2854
+ const totalPages = Math.max(1, rawPages.length);
2855
+ let currentPage = 1;
2531
2856
  const container = document.createElement("div");
2532
2857
  container.style.width = "100%";
2533
2858
  container.style.height = "100%";
@@ -2546,25 +2871,66 @@ var CodePlugin = class {
2546
2871
  pre.style.wordBreak = "break-all";
2547
2872
  const code = document.createElement("code");
2548
2873
  const ext = (ctx.metadata.extension || "").replace(".", "");
2549
- try {
2550
- if (ext && hljs__default.default.getLanguage(ext)) {
2551
- code.innerHTML = hljs__default.default.highlight(text, { language: ext }).value;
2552
- } else {
2553
- code.innerHTML = hljs__default.default.highlightAuto(text).value;
2874
+ const renderCodePage = (text) => {
2875
+ try {
2876
+ if (ext && hljs__default.default.getLanguage(ext)) {
2877
+ code.innerHTML = hljs__default.default.highlight(text, { language: ext }).value;
2878
+ } else {
2879
+ code.innerHTML = hljs__default.default.highlightAuto(text).value;
2880
+ }
2881
+ } catch {
2882
+ code.textContent = text;
2554
2883
  }
2555
- } catch {
2556
- code.textContent = text;
2557
- }
2884
+ };
2885
+ renderCodePage(rawPages[0] || fullText);
2558
2886
  pre.appendChild(code);
2559
2887
  container.appendChild(pre);
2888
+ let indicator = null;
2889
+ if (totalPages > 1) {
2890
+ indicator = document.createElement("div");
2891
+ indicator.className = "fp-code-page-indicator";
2892
+ indicator.style.position = "sticky";
2893
+ indicator.style.bottom = "16px";
2894
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
2895
+ indicator.style.backdropFilter = "blur(8px)";
2896
+ indicator.style.color = "#f8fafc";
2897
+ indicator.style.fontSize = "12px";
2898
+ indicator.style.fontWeight = "600";
2899
+ indicator.style.padding = "5px 14px";
2900
+ indicator.style.borderRadius = "20px";
2901
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
2902
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
2903
+ indicator.style.zIndex = "10";
2904
+ indicator.style.userSelect = "none";
2905
+ indicator.style.pointerEvents = "none";
2906
+ indicator.style.textAlign = "center";
2907
+ indicator.style.width = "fit-content";
2908
+ indicator.style.margin = "16px auto 0";
2909
+ container.appendChild(indicator);
2910
+ }
2911
+ const showPage = (pageNum) => {
2912
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
2913
+ renderCodePage(rawPages[currentPage - 1] || fullText);
2914
+ if (indicator) {
2915
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
2916
+ }
2917
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
2918
+ };
2919
+ if (totalPages > 1) {
2920
+ showPage(1);
2921
+ }
2560
2922
  ctx.container.appendChild(container);
2561
2923
  const cleanup = () => {
2924
+ indicator?.remove();
2562
2925
  container.remove();
2563
2926
  ctx.container.innerHTML = "";
2564
2927
  };
2565
2928
  ctx.signal.addEventListener("abort", cleanup);
2566
2929
  return {
2567
2930
  destroy: cleanup,
2931
+ getPageCount: () => totalPages,
2932
+ getCurrentPage: () => currentPage,
2933
+ goToPage: (page) => showPage(page),
2568
2934
  zoomIn: () => {
2569
2935
  fontSize = Math.min(32, fontSize + 2);
2570
2936
  pre.style.fontSize = `${fontSize}px`;
@@ -2592,7 +2958,7 @@ var CodePlugin = class {
2592
2958
  window.print();
2593
2959
  },
2594
2960
  copy: () => {
2595
- navigator.clipboard?.writeText(text);
2961
+ navigator.clipboard?.writeText(fullText);
2596
2962
  }
2597
2963
  };
2598
2964
  }
@@ -3027,7 +3393,10 @@ var PptxPlugin = class {
3027
3393
  supports(file) {
3028
3394
  const ext = file.metadata.extension?.toLowerCase();
3029
3395
  const mime = file.metadata.mimeType?.toLowerCase();
3030
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3396
+ if (ext) {
3397
+ return this.extensions.includes(ext);
3398
+ }
3399
+ return this.mimeTypes.includes(mime || "");
3031
3400
  }
3032
3401
  getToolbarActions(instance) {
3033
3402
  return [
@@ -3411,7 +3780,31 @@ var RtfPlugin = class {
3411
3780
  return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3412
3781
  }
3413
3782
  getToolbarActions(instance) {
3414
- return [
3783
+ const totalPages = instance.getPageCount?.() ?? 1;
3784
+ const actions = [];
3785
+ if (totalPages > 1) {
3786
+ actions.push({
3787
+ id: "page-nav",
3788
+ icon: "",
3789
+ label: "Page Navigation",
3790
+ type: "page-nav",
3791
+ group: "navigation",
3792
+ value: instance.getCurrentPage?.() ?? 1,
3793
+ max: totalPages,
3794
+ execute: (action, page) => {
3795
+ const cur = instance.getCurrentPage?.() ?? 1;
3796
+ const max = instance.getPageCount?.() ?? 1;
3797
+ if (action === "prev") {
3798
+ if (cur > 1) instance.goToPage?.(cur - 1);
3799
+ } else if (action === "next") {
3800
+ if (cur < max) instance.goToPage?.(cur + 1);
3801
+ } else if (typeof page === "number") {
3802
+ instance.goToPage?.(page);
3803
+ }
3804
+ }
3805
+ });
3806
+ }
3807
+ actions.push(
3415
3808
  {
3416
3809
  id: "zoom-out",
3417
3810
  icon: "zoom-out",
@@ -3452,7 +3845,8 @@ var RtfPlugin = class {
3452
3845
  group: "actions",
3453
3846
  execute: () => instance.print?.()
3454
3847
  }
3455
- ];
3848
+ );
3849
+ return actions;
3456
3850
  }
3457
3851
  async render(ctx) {
3458
3852
  const wrapper = document.createElement("div");
@@ -3471,25 +3865,82 @@ var RtfPlugin = class {
3471
3865
  ctx.container.style.backgroundColor = "#f1f5f9";
3472
3866
  ctx.container.appendChild(wrapper);
3473
3867
  let scale = 1;
3868
+ let pageElements = [];
3474
3869
  try {
3475
- const doc = new rtf_js.RTFJS.Document(ctx.buffer, {});
3870
+ if (typeof RTFJS__namespace.loggingEnabled === "function") {
3871
+ RTFJS__namespace.loggingEnabled(false);
3872
+ }
3873
+ const doc = new RTFJS__namespace.Document(ctx.buffer, {});
3476
3874
  const htmlElements = await doc.render();
3477
- for (const el of htmlElements) {
3875
+ pageElements = htmlElements;
3876
+ for (let i = 0; i < htmlElements.length; i++) {
3877
+ const el = htmlElements[i];
3878
+ el.style.display = i === 0 ? "block" : "none";
3478
3879
  wrapper.appendChild(el);
3479
3880
  }
3480
3881
  } catch (err) {
3481
3882
  console.warn("[RtfPlugin] RTF render error, fallback text:", err);
3482
3883
  const text = new TextDecoder("latin1").decode(ctx.buffer);
3483
3884
  const clean = text.replace(/\\par[d]?/g, "\n").replace(/\\[a-zA-Z0-9\-]+/g, "").replace(/[{}]/g, "");
3484
- wrapper.innerHTML = `<pre style="white-space: pre-wrap; font-family: serif; color: #333;">${clean}</pre>`;
3885
+ const pre = document.createElement("pre");
3886
+ pre.style.whiteSpace = "pre-wrap";
3887
+ pre.style.fontFamily = "serif";
3888
+ pre.style.color = "#333";
3889
+ pre.textContent = clean;
3890
+ wrapper.appendChild(pre);
3891
+ pageElements = [pre];
3892
+ }
3893
+ const totalPages = Math.max(1, pageElements.length);
3894
+ let currentPage = 1;
3895
+ let indicator = null;
3896
+ if (totalPages > 1) {
3897
+ indicator = document.createElement("div");
3898
+ indicator.className = "fp-rtf-page-indicator";
3899
+ indicator.style.position = "sticky";
3900
+ indicator.style.bottom = "16px";
3901
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
3902
+ indicator.style.backdropFilter = "blur(8px)";
3903
+ indicator.style.color = "#f8fafc";
3904
+ indicator.style.fontSize = "12px";
3905
+ indicator.style.fontWeight = "600";
3906
+ indicator.style.padding = "5px 14px";
3907
+ indicator.style.borderRadius = "20px";
3908
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
3909
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
3910
+ indicator.style.zIndex = "10";
3911
+ indicator.style.userSelect = "none";
3912
+ indicator.style.pointerEvents = "none";
3913
+ indicator.style.textAlign = "center";
3914
+ indicator.style.width = "fit-content";
3915
+ indicator.style.margin = "16px auto 0";
3916
+ ctx.container.appendChild(indicator);
3917
+ }
3918
+ const showPage = (pageNum) => {
3919
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
3920
+ if (totalPages > 1) {
3921
+ pageElements.forEach((el, idx) => {
3922
+ el.style.display = idx + 1 === currentPage ? "block" : "none";
3923
+ });
3924
+ }
3925
+ if (indicator) {
3926
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
3927
+ }
3928
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
3929
+ };
3930
+ if (totalPages > 1) {
3931
+ showPage(1);
3485
3932
  }
3486
3933
  const cleanup = () => {
3934
+ indicator?.remove();
3487
3935
  wrapper.remove();
3488
3936
  ctx.container.innerHTML = "";
3489
3937
  };
3490
3938
  ctx.signal.addEventListener("abort", cleanup);
3491
3939
  return {
3492
3940
  destroy: cleanup,
3941
+ getPageCount: () => totalPages,
3942
+ getCurrentPage: () => currentPage,
3943
+ goToPage: (page) => showPage(page),
3493
3944
  zoomIn: () => {
3494
3945
  scale += 0.1;
3495
3946
  wrapper.style.transform = `scale(${scale})`;
@@ -3670,45 +4121,48 @@ var OpenDocumentPlugin = class {
3670
4121
  supports(file) {
3671
4122
  const ext = file.metadata.extension?.toLowerCase();
3672
4123
  const mime = file.metadata.mimeType?.toLowerCase();
3673
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4124
+ if (ext) {
4125
+ return this.extensions.includes(ext);
4126
+ }
4127
+ return this.mimeTypes.includes(mime || "");
3674
4128
  }
3675
4129
  getToolbarActions(instance) {
3676
4130
  const isPresentation = instance.isPresentation;
4131
+ const totalPages = instance.getPageCount?.() ?? 1;
3677
4132
  const actions = [];
3678
- if (isPresentation) {
3679
- actions.push(
3680
- {
4133
+ if (isPresentation || totalPages > 1) {
4134
+ if (isPresentation) {
4135
+ actions.push({
3681
4136
  id: "thumbnails",
3682
4137
  icon: "thumbnails",
3683
4138
  label: "Slide Thumbnails",
3684
4139
  type: "button",
3685
4140
  group: "navigation",
3686
4141
  execute: () => instance.toggleThumbnails?.()
3687
- },
3688
- {
3689
- id: "page-nav",
3690
- icon: "",
3691
- label: "Slide Navigation",
3692
- type: "page-nav",
3693
- group: "navigation",
3694
- value: instance.getCurrentPage?.() ?? 1,
3695
- max: instance.getPageCount?.() ?? 1,
3696
- execute: (action, page) => {
3697
- if (action === "prev") {
3698
- const cur = instance.getCurrentPage?.() ?? 1;
3699
- if (cur > 1) instance.goToPage?.(cur - 1);
3700
- } else if (action === "next") {
3701
- const cur = instance.getCurrentPage?.() ?? 1;
3702
- const total = instance.getPageCount?.() ?? 1;
3703
- if (cur < total) instance.goToPage?.(cur + 1);
3704
- } else if (typeof page === "number") {
3705
- instance.goToPage?.(page);
3706
- } else if (typeof action === "number") {
3707
- instance.goToPage?.(action);
3708
- }
4142
+ });
4143
+ }
4144
+ actions.push({
4145
+ id: "page-nav",
4146
+ icon: "",
4147
+ label: isPresentation ? "Slide Navigation" : "Page Navigation",
4148
+ type: "page-nav",
4149
+ group: "navigation",
4150
+ value: instance.getCurrentPage?.() ?? 1,
4151
+ max: totalPages,
4152
+ execute: (action, page) => {
4153
+ const cur = instance.getCurrentPage?.() ?? 1;
4154
+ const max = instance.getPageCount?.() ?? 1;
4155
+ if (action === "prev") {
4156
+ if (cur > 1) instance.goToPage?.(cur - 1);
4157
+ } else if (action === "next") {
4158
+ if (cur < max) instance.goToPage?.(cur + 1);
4159
+ } else if (typeof page === "number") {
4160
+ instance.goToPage?.(page);
4161
+ } else if (typeof action === "number") {
4162
+ instance.goToPage?.(action);
3709
4163
  }
3710
4164
  }
3711
- );
4165
+ });
3712
4166
  }
3713
4167
  actions.push(
3714
4168
  {
@@ -4085,10 +4539,37 @@ var DocPlugin = class {
4085
4539
  supports(file) {
4086
4540
  const ext = file.metadata.extension?.toLowerCase();
4087
4541
  const mime = file.metadata.mimeType?.toLowerCase();
4088
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4542
+ if (ext) {
4543
+ return this.extensions.includes(ext);
4544
+ }
4545
+ return this.mimeTypes.includes(mime || "");
4089
4546
  }
4090
4547
  getToolbarActions(instance) {
4091
- return [
4548
+ const totalPages = instance.getPageCount?.() ?? 1;
4549
+ const actions = [];
4550
+ if (totalPages > 1) {
4551
+ actions.push({
4552
+ id: "page-nav",
4553
+ icon: "",
4554
+ label: "Page Navigation",
4555
+ type: "page-nav",
4556
+ group: "navigation",
4557
+ value: instance.getCurrentPage?.() ?? 1,
4558
+ max: totalPages,
4559
+ execute: (action, page) => {
4560
+ const cur = instance.getCurrentPage?.() ?? 1;
4561
+ const max = instance.getPageCount?.() ?? 1;
4562
+ if (action === "prev") {
4563
+ if (cur > 1) instance.goToPage?.(cur - 1);
4564
+ } else if (action === "next") {
4565
+ if (cur < max) instance.goToPage?.(cur + 1);
4566
+ } else if (typeof page === "number") {
4567
+ instance.goToPage?.(page);
4568
+ }
4569
+ }
4570
+ });
4571
+ }
4572
+ actions.push(
4092
4573
  {
4093
4574
  id: "zoom-out",
4094
4575
  icon: "zoom-out",
@@ -4137,7 +4618,8 @@ var DocPlugin = class {
4137
4618
  group: "actions",
4138
4619
  execute: () => instance.print?.()
4139
4620
  }
4140
- ];
4621
+ );
4622
+ return actions;
4141
4623
  }
4142
4624
  async render(ctx) {
4143
4625
  const container = document.createElement("div");
@@ -4164,6 +4646,7 @@ var DocPlugin = class {
4164
4646
  ctx.container.appendChild(container);
4165
4647
  let scale = 1;
4166
4648
  let extractedRawText = "";
4649
+ let isFallback = false;
4167
4650
  try {
4168
4651
  const cfbf = new CfbfReader(ctx.buffer);
4169
4652
  const wordDocStream = cfbf.readStream("WordDocument");
@@ -4177,26 +4660,89 @@ var DocPlugin = class {
4177
4660
  const tableStream = cfbf.readStream(tableName);
4178
4661
  const text = this.extractDocText(wordDocStream, tableStream);
4179
4662
  extractedRawText = text;
4180
- wrapper.innerHTML = this.formatDocToHtml(text, ctx.metadata.name || "Document");
4181
4663
  } catch (err) {
4182
4664
  console.warn("[DocPlugin] Binary parsing error, fallback text:", err);
4183
4665
  const fallback = this.heuristicTextExtraction(ctx.buffer);
4184
4666
  extractedRawText = fallback;
4185
- wrapper.innerHTML = `
4186
- <div style="border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 24px;">
4187
- <h2 style="margin: 0 0 6px; font-size: 20px; color: #334155;">${ctx.metadata.name || "Word Document (.doc)"}</h2>
4188
- <span style="font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px;">Legacy Word 97-2003 Binary Preview</span>
4189
- </div>
4190
- ${this.formatDocToHtml(fallback, ctx.metadata.name || "Document")}
4191
- `;
4667
+ isFallback = true;
4668
+ }
4669
+ const rawPages = this.splitIntoPages(extractedRawText);
4670
+ const totalPages = Math.max(1, rawPages.length);
4671
+ let currentPage = 1;
4672
+ wrapper.innerHTML = "";
4673
+ const pageCards = [];
4674
+ for (let i = 0; i < totalPages; i++) {
4675
+ const pageCard = document.createElement("div");
4676
+ pageCard.className = "fp-doc-page-card";
4677
+ pageCard.style.backgroundColor = "#ffffff";
4678
+ pageCard.style.boxShadow = "0 2px 12px rgba(0,0,0,0.08)";
4679
+ pageCard.style.borderRadius = "4px";
4680
+ pageCard.style.padding = "56px 48px";
4681
+ pageCard.style.minHeight = "100%";
4682
+ pageCard.style.display = i === 0 ? "block" : "none";
4683
+ if (isFallback && i === 0) {
4684
+ pageCard.innerHTML = `
4685
+ <div style="border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 24px;">
4686
+ <h2 style="margin: 0 0 6px; font-size: 20px; color: #334155;">${DOMPurify2__default.default.sanitize(ctx.metadata.name || "Word Document (.doc)")}</h2>
4687
+ <span style="font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px;">Legacy Word 97-2003 Binary Preview</span>
4688
+ </div>
4689
+ ${this.formatDocToHtml(rawPages[i], ctx.metadata.name || "Document")}
4690
+ `;
4691
+ } else {
4692
+ pageCard.innerHTML = this.formatDocToHtml(rawPages[i], ctx.metadata.name || "Document");
4693
+ }
4694
+ wrapper.appendChild(pageCard);
4695
+ pageCards.push(pageCard);
4696
+ }
4697
+ let indicator = null;
4698
+ if (totalPages > 1) {
4699
+ indicator = document.createElement("div");
4700
+ indicator.className = "fp-doc-page-indicator";
4701
+ indicator.style.position = "sticky";
4702
+ indicator.style.bottom = "16px";
4703
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
4704
+ indicator.style.backdropFilter = "blur(8px)";
4705
+ indicator.style.color = "#f8fafc";
4706
+ indicator.style.fontSize = "12px";
4707
+ indicator.style.fontWeight = "600";
4708
+ indicator.style.padding = "5px 14px";
4709
+ indicator.style.borderRadius = "20px";
4710
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
4711
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
4712
+ indicator.style.zIndex = "10";
4713
+ indicator.style.userSelect = "none";
4714
+ indicator.style.pointerEvents = "none";
4715
+ indicator.style.textAlign = "center";
4716
+ indicator.style.width = "fit-content";
4717
+ indicator.style.margin = "16px auto 0";
4718
+ container.appendChild(indicator);
4719
+ }
4720
+ const showPage = (pageNum) => {
4721
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
4722
+ if (totalPages > 1) {
4723
+ pageCards.forEach((card, idx) => {
4724
+ card.style.display = idx + 1 === currentPage ? "block" : "none";
4725
+ });
4726
+ }
4727
+ if (indicator) {
4728
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
4729
+ }
4730
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
4731
+ };
4732
+ if (totalPages > 1) {
4733
+ showPage(1);
4192
4734
  }
4193
4735
  const cleanup = () => {
4736
+ indicator?.remove();
4194
4737
  container.remove();
4195
4738
  ctx.container.innerHTML = "";
4196
4739
  };
4197
4740
  ctx.signal.addEventListener("abort", cleanup);
4198
4741
  return {
4199
4742
  destroy: cleanup,
4743
+ getPageCount: () => totalPages,
4744
+ getCurrentPage: () => currentPage,
4745
+ goToPage: (page) => showPage(page),
4200
4746
  zoomIn: () => {
4201
4747
  scale += 0.1;
4202
4748
  wrapper.style.transform = `scale(${scale})`;
@@ -4308,31 +4854,36 @@ var DocPlugin = class {
4308
4854
  * Scans a byte array for continuous sequences of readable characters (ANSI and UTF-16LE)
4309
4855
  */
4310
4856
  extractStringsFromBytes(bytes) {
4311
- const chars = [];
4312
- const len = bytes.length;
4313
- for (let i = 0; i < len; i++) {
4314
- const b = bytes[i];
4315
- if (b === 13 || b === 10 || b === 9 || b >= 32 && b <= 126 || b >= 160 && b <= 255) {
4316
- chars.push(String.fromCharCode(b));
4317
- } else if (b === 0 && i + 1 < len && bytes[i + 1] >= 32 && bytes[i + 1] <= 126) {
4318
- chars.push(String.fromCharCode(bytes[i + 1]));
4319
- i++;
4320
- } else if (b === 7) {
4321
- chars.push(" ");
4322
- } else if (b === 12) {
4323
- chars.push("\n\n---PAGE---\n\n");
4857
+ const rawAnsi = new TextDecoder("latin1").decode(bytes);
4858
+ const rawUtf16 = new TextDecoder("utf-16le", { fatal: false }).decode(bytes);
4859
+ const ansiRuns = rawAnsi.match(/[\x20-\x7E\t\r\n]{4,}/g) || [];
4860
+ const utf16Runs = rawUtf16.match(/[\x20-\x7E\t\r\n]{4,}/g) || [];
4861
+ const candidateLines = [];
4862
+ const seen = /* @__PURE__ */ new Set();
4863
+ for (const run of [...ansiRuns, ...utf16Runs]) {
4864
+ const trimmed = run.trim();
4865
+ if (trimmed.length >= 4 && /[a-zA-Z]/.test(trimmed) && !seen.has(trimmed)) {
4866
+ 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)) {
4867
+ seen.add(trimmed);
4868
+ candidateLines.push(trimmed);
4869
+ }
4324
4870
  }
4325
4871
  }
4326
- return chars.join("");
4872
+ return candidateLines.join("\n\n");
4327
4873
  }
4328
4874
  heuristicTextExtraction(buffer) {
4329
4875
  return this.extractStringsFromBytes(new Uint8Array(buffer));
4330
4876
  }
4877
+ splitIntoPages(text) {
4878
+ if (!text) return [""];
4879
+ 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);
4880
+ return parts.length > 0 ? parts : [text];
4881
+ }
4331
4882
  /**
4332
4883
  * Format cleaned extracted text into attractive HTML paragraphs, headings, and lists
4333
4884
  */
4334
4885
  formatDocToHtml(text, filename) {
4335
- const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x0C/g, "\n\n").split("\n");
4886
+ const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").split("\n");
4336
4887
  let html = "";
4337
4888
  let inList = false;
4338
4889
  for (const rawLine of lines) {
@@ -4378,14 +4929,17 @@ function docPlugin() {
4378
4929
  }
4379
4930
  var PptPlugin = class {
4380
4931
  id = "ppt";
4381
- name = "Legacy PowerPoint Presentation (.ppt, .pps, .pot)";
4932
+ name = "PowerPoint Presentation (.ppt, .pps, .pot)";
4382
4933
  extensions = [".ppt", ".pps", ".pot"];
4383
4934
  mimeTypes = ["application/vnd.ms-powerpoint"];
4384
- weight = 75;
4935
+ weight = 85;
4385
4936
  supports(file) {
4386
4937
  const ext = file.metadata.extension?.toLowerCase();
4387
4938
  const mime = file.metadata.mimeType?.toLowerCase();
4388
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4939
+ if (ext) {
4940
+ return this.extensions.includes(ext);
4941
+ }
4942
+ return this.mimeTypes.includes(mime || "");
4389
4943
  }
4390
4944
  getToolbarActions(instance) {
4391
4945
  return [
@@ -4473,19 +5027,15 @@ var PptPlugin = class {
4473
5027
  container.style.alignItems = "center";
4474
5028
  container.style.padding = "32px 16px";
4475
5029
  container.style.backgroundColor = "#0f172a";
5030
+ container.style.boxSizing = "border-box";
4476
5031
  const slideCard = document.createElement("div");
4477
5032
  slideCard.className = "fp-ppt-slide-card";
4478
5033
  slideCard.style.width = "960px";
4479
- slideCard.style.maxWidth = "90%";
5034
+ slideCard.style.maxWidth = "92%";
4480
5035
  slideCard.style.aspectRatio = "16 / 9";
4481
5036
  slideCard.style.backgroundColor = "#ffffff";
4482
- slideCard.style.boxShadow = "0 8px 30px rgba(0,0,0,0.3)";
5037
+ slideCard.style.boxShadow = "0 12px 40px rgba(0,0,0,0.35)";
4483
5038
  slideCard.style.borderRadius = "8px";
4484
- slideCard.style.padding = "48px";
4485
- slideCard.style.display = "flex";
4486
- slideCard.style.flexDirection = "column";
4487
- slideCard.style.justifyContent = "center";
4488
- slideCard.style.alignItems = "center";
4489
5039
  slideCard.style.boxSizing = "border-box";
4490
5040
  slideCard.style.position = "relative";
4491
5041
  slideCard.style.overflow = "hidden";
@@ -4496,21 +5046,25 @@ var PptPlugin = class {
4496
5046
  let scale = 1;
4497
5047
  let currentSlide = 1;
4498
5048
  let slides = [];
5049
+ const createdBlobUrls = [];
4499
5050
  try {
4500
5051
  const cfbf = new CfbfReader(ctx.buffer);
4501
5052
  const pptStream = cfbf.readStream("PowerPoint Document");
4502
5053
  if (!pptStream || pptStream.length < 512) {
4503
5054
  throw new Error("PowerPoint Document stream not found in CFBF container");
4504
5055
  }
4505
- slides = this.extractSlides(pptStream);
5056
+ const pictures = this.extractPictures(cfbf, createdBlobUrls);
5057
+ slides = this.extractSlides(pptStream, pictures);
4506
5058
  } catch (err) {
4507
5059
  console.warn("[PptPlugin] Error extracting binary slides:", err);
4508
5060
  }
4509
5061
  if (slides.length === 0) {
4510
5062
  slides = [
4511
5063
  {
5064
+ slideIndex: 1,
4512
5065
  title: ctx.metadata.name || "PowerPoint Presentation",
4513
- texts: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"]
5066
+ paragraphs: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"],
5067
+ tableColumns: []
4514
5068
  }
4515
5069
  ];
4516
5070
  }
@@ -4519,23 +5073,73 @@ var PptPlugin = class {
4519
5073
  currentSlide = idx;
4520
5074
  const s = slides[idx - 1];
4521
5075
  if (!s) return;
5076
+ let contentHtml = "";
5077
+ if (s.pictureUrl) {
5078
+ contentHtml = `
5079
+ <div style="flex: 1; display: flex; justify-content: center; align-items: center; padding: 12px; overflow: hidden;">
5080
+ <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;" />
5081
+ </div>
5082
+ `;
5083
+ } else if (s.tableColumns.length > 0) {
5084
+ const cols = s.tableColumns;
5085
+ contentHtml = `
5086
+ <div style="flex: 1; overflow: auto; padding: 8px 0;">
5087
+ <table style="width: 100%; border-collapse: collapse; border: 1px solid #cbd5e1; border-radius: 6px; overflow: hidden; background: #ffffff;">
5088
+ <thead>
5089
+ <tr style="background: #e2e8f0; color: #1e293b; font-weight: 600; font-size: 14px;">
5090
+ ${cols.map((c) => `<th style="padding: 12px 16px; border: 1px solid #cbd5e1; text-align: left;">${DOMPurify2__default.default.sanitize(c)}</th>`).join("")}
5091
+ </tr>
5092
+ </thead>
5093
+ <tbody>
5094
+ ${[1, 2, 3, 4, 5].map((rowIdx) => `
5095
+ <tr style="${rowIdx % 2 === 0 ? "background: #f8fafc;" : "background: #ffffff;"}">
5096
+ ${cols.map((_, cIdx) => `<td style="padding: 10px 16px; border: 1px solid #e2e8f0; font-size: 13px; color: #334155;">Data ${rowIdx}-${cIdx + 1}</td>`).join("")}
5097
+ </tr>
5098
+ `).join("")}
5099
+ </tbody>
5100
+ </table>
5101
+ </div>
5102
+ `;
5103
+ } else {
5104
+ const pTags = s.paragraphs.map((p) => {
5105
+ const lines = p.split(/[\r\n]+/).map((l) => l.trim()).filter(Boolean);
5106
+ 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("");
5107
+ }).join("");
5108
+ contentHtml = `
5109
+ <div style="flex: 1; overflow: auto; padding: 4px 8px; display: flex; flex-direction: column; justify-content: flex-start;">
5110
+ ${pTags || '<p style="color: #64748b; font-style: italic;">No additional text on this slide</p>'}
5111
+ </div>
5112
+ `;
5113
+ }
4522
5114
  slideCard.innerHTML = `
4523
- <div style="position: absolute; top: 20px; right: 24px; font-size: 12px; color: #94a3b8; font-weight: 600;">
4524
- Slide ${idx} of ${totalSlides}
4525
- </div>
4526
- <div style="text-align: center; width: 100%;">
4527
- <h1 style="font-size: ${idx === 1 ? "36px" : "28px"}; color: #1e3a8a; margin: 0 0 24px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-weight: 700;">
4528
- ${DOMPurify2__default.default.sanitize(s.title || `Slide ${idx}`)}
4529
- </h1>
4530
- <div style="display: flex; flex-direction: column; gap: 12px; max-width: 80%; margin: 0 auto; text-align: ${idx === 1 ? "center" : "left"};">
4531
- ${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("")}
5115
+ <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;">
5116
+ <!-- Header Banner matching PowerPoint design -->
5117
+ <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;">
5118
+ <h1 style="margin: 0; font-size: 26px; font-weight: 700; color: #1e293b; letter-spacing: -0.3px;">
5119
+ ${DOMPurify2__default.default.sanitize(s.title)}
5120
+ </h1>
5121
+ ${s.subtitle ? `
5122
+ <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);">
5123
+ ${DOMPurify2__default.default.sanitize(s.subtitle)}
5124
+ </span>
5125
+ ` : `
5126
+ <span style="font-size: 12px; color: #365314; font-weight: 600;">
5127
+ Slide ${idx} / ${totalSlides}
5128
+ </span>
5129
+ `}
4532
5130
  </div>
5131
+
5132
+ <!-- Slide Content -->
5133
+ ${contentHtml}
4533
5134
  </div>
4534
5135
  `;
4535
5136
  ctx.emit("page-change", { page: currentSlide, total: totalSlides });
4536
5137
  };
4537
5138
  renderSlide(1);
4538
5139
  const cleanup = () => {
5140
+ for (const u of createdBlobUrls) {
5141
+ URL.revokeObjectURL(u);
5142
+ }
4539
5143
  container.remove();
4540
5144
  ctx.container.innerHTML = "";
4541
5145
  };
@@ -4575,13 +5179,48 @@ var PptPlugin = class {
4575
5179
  if (!ctx2d) return;
4576
5180
  canvas.width = 160;
4577
5181
  canvas.height = 90;
4578
- ctx2d.fillStyle = "#ffffff";
5182
+ ctx2d.fillStyle = "#334155";
4579
5183
  ctx2d.fillRect(0, 0, 160, 90);
4580
- ctx2d.fillStyle = "#1e3a8a";
4581
- ctx2d.font = "bold 11px sans-serif";
4582
- ctx2d.textAlign = "center";
4583
- const title = s.title.slice(0, 18) || `Slide ${idx + 1}`;
4584
- ctx2d.fillText(title, 80, 50);
5184
+ ctx2d.fillStyle = "#ffffff";
5185
+ ctx2d.fillRect(3, 3, 154, 84);
5186
+ ctx2d.fillStyle = "#84cc16";
5187
+ ctx2d.fillRect(6, 6, 148, 18);
5188
+ ctx2d.fillStyle = "#1e293b";
5189
+ ctx2d.font = "bold 9px sans-serif";
5190
+ ctx2d.textAlign = "left";
5191
+ const displayTitle = s.title.length > 18 ? s.title.slice(0, 16) + ".." : s.title;
5192
+ ctx2d.fillText(displayTitle, 10, 19);
5193
+ if (s.subtitle) {
5194
+ ctx2d.fillStyle = "#38bdf8";
5195
+ ctx2d.fillRect(116, 9, 34, 12);
5196
+ ctx2d.fillStyle = "#ffffff";
5197
+ ctx2d.font = "bold 7px sans-serif";
5198
+ ctx2d.textAlign = "center";
5199
+ ctx2d.fillText(s.subtitle.slice(0, 7), 133, 18);
5200
+ }
5201
+ if (s.pictureUrl) {
5202
+ ctx2d.fillStyle = "#3b82f6";
5203
+ ctx2d.fillRect(52, 34, 56, 42);
5204
+ ctx2d.fillStyle = "#ffffff";
5205
+ ctx2d.font = "8px sans-serif";
5206
+ ctx2d.textAlign = "center";
5207
+ ctx2d.fillText("Chart", 80, 58);
5208
+ } else if (s.tableColumns.length > 0) {
5209
+ ctx2d.strokeStyle = "#cbd5e1";
5210
+ ctx2d.lineWidth = 1;
5211
+ ctx2d.strokeRect(14, 32, 132, 46);
5212
+ for (let l = 1; l <= 3; l++) {
5213
+ ctx2d.beginPath();
5214
+ ctx2d.moveTo(14, 32 + l * 11);
5215
+ ctx2d.lineTo(146, 32 + l * 11);
5216
+ ctx2d.stroke();
5217
+ }
5218
+ } else {
5219
+ ctx2d.fillStyle = "#94a3b8";
5220
+ for (let l = 0; l < 4; l++) {
5221
+ ctx2d.fillRect(14, 34 + l * 10, 132 - l * 14, 4);
5222
+ }
5223
+ }
4585
5224
  }
4586
5225
  }));
4587
5226
  },
@@ -4599,66 +5238,165 @@ var PptPlugin = class {
4599
5238
  }
4600
5239
  };
4601
5240
  }
5241
+ /**
5242
+ * Extract PNG and JPEG images from the Pictures stream
5243
+ */
5244
+ extractPictures(cfbf, createdUrls) {
5245
+ const urls = [];
5246
+ try {
5247
+ const picStream = cfbf.readStream("Pictures");
5248
+ if (!picStream || picStream.length < 32) return urls;
5249
+ const pBuf = new Uint8Array(picStream);
5250
+ const pngSig = [137, 80, 78, 71, 13, 10, 26, 10];
5251
+ const iendSig = [73, 69, 78, 68, 174, 66, 96, 130];
5252
+ for (let i = 0; i <= pBuf.length - 8; i++) {
5253
+ let match = true;
5254
+ for (let j = 0; j < 8; j++) {
5255
+ if (pBuf[i + j] !== pngSig[j]) {
5256
+ match = false;
5257
+ break;
5258
+ }
5259
+ }
5260
+ if (match) {
5261
+ let endIdx = -1;
5262
+ for (let k = i + 8; k <= pBuf.length - 8; k++) {
5263
+ let endMatch = true;
5264
+ for (let j = 0; j < 8; j++) {
5265
+ if (pBuf[k + j] !== iendSig[j]) {
5266
+ endMatch = false;
5267
+ break;
5268
+ }
5269
+ }
5270
+ if (endMatch) {
5271
+ endIdx = k + 8;
5272
+ break;
5273
+ }
5274
+ }
5275
+ if (endIdx !== -1) {
5276
+ const pngBytes = pBuf.subarray(i, endIdx);
5277
+ const blob = new Blob([pngBytes], { type: "image/png" });
5278
+ const url = URL.createObjectURL(blob);
5279
+ urls.push(url);
5280
+ createdUrls.push(url);
5281
+ i = endIdx;
5282
+ }
5283
+ }
5284
+ }
5285
+ for (let i = 0; i <= pBuf.length - 3; i++) {
5286
+ if (pBuf[i] === 255 && pBuf[i + 1] === 216 && pBuf[i + 2] === 255) {
5287
+ let endIdx = -1;
5288
+ for (let k = i + 3; k < pBuf.length - 1; k++) {
5289
+ if (pBuf[k] === 255 && pBuf[k + 1] === 217) {
5290
+ endIdx = k + 2;
5291
+ break;
5292
+ }
5293
+ }
5294
+ if (endIdx !== -1) {
5295
+ const jpgBytes = pBuf.subarray(i, endIdx);
5296
+ const blob = new Blob([jpgBytes], { type: "image/jpeg" });
5297
+ const url = URL.createObjectURL(blob);
5298
+ urls.push(url);
5299
+ createdUrls.push(url);
5300
+ i = endIdx;
5301
+ }
5302
+ }
5303
+ }
5304
+ } catch (err) {
5305
+ console.warn("[PptPlugin] Error extracting pictures:", err);
5306
+ }
5307
+ return urls;
5308
+ }
4602
5309
  /**
4603
5310
  * Traverse PowerPoint binary stream records ([MS-PPT]) and extract text chunks per slide
4604
5311
  */
4605
- extractSlides(stream) {
5312
+ extractSlides(stream, pictures) {
4606
5313
  const view = new DataView(stream.buffer, stream.byteOffset, stream.byteLength);
4607
5314
  const len = stream.length;
4608
5315
  let offset = 0;
4609
5316
  const slides = [];
4610
- let currentSlideTexts = [];
5317
+ let picIdx = 0;
4611
5318
  while (offset + 8 <= len) {
4612
5319
  const recVerInst = view.getUint16(offset, true);
4613
5320
  const recType = view.getUint16(offset + 2, true);
4614
5321
  const recLen = view.getUint32(offset + 4, true);
5322
+ const isContainer = (recVerInst & 15) === 15;
4615
5323
  if (recType === 1006) {
4616
- if (currentSlideTexts.length > 0) {
4617
- const title = currentSlideTexts[0] || "Slide";
4618
- const texts = currentSlideTexts.slice(1);
4619
- slides.push({ title, texts });
4620
- currentSlideTexts = [];
5324
+ const slideEnd = Math.min(len, offset + 8 + recLen);
5325
+ const rawTexts = [];
5326
+ let hasOle = false;
5327
+ let sOff = offset + 8;
5328
+ while (sOff + 8 <= slideEnd) {
5329
+ const cVerInst = view.getUint16(sOff, true);
5330
+ const cType = view.getUint16(sOff + 2, true);
5331
+ const cLen = view.getUint32(sOff + 4, true);
5332
+ const cIsContainer = (cVerInst & 15) === 15;
5333
+ if ((cType === 4008 || cType === 3998) && cLen > 0 && sOff + 8 + cLen <= slideEnd) {
5334
+ const bytes = stream.subarray(sOff + 8, sOff + 8 + cLen);
5335
+ const txt = new TextDecoder("latin1").decode(bytes).trim();
5336
+ if (txt && !/^[\x00-\x1F]+$/.test(txt) && !txt.includes("[Content_Types]") && !txt.includes("_rels/")) {
5337
+ rawTexts.push(txt);
5338
+ }
5339
+ } else if (cType === 3999 && cLen > 0 && sOff + 8 + cLen <= slideEnd) {
5340
+ const bytes = stream.subarray(sOff + 8, sOff + 8 + cLen);
5341
+ const txt = new TextDecoder("utf-16le").decode(bytes).trim();
5342
+ if (txt && !/^[\x00-\x1F]+$/.test(txt) && !txt.includes("[Content_Types]") && !txt.includes("_rels/")) {
5343
+ rawTexts.push(txt);
5344
+ }
5345
+ } else if (cType === 3009 || cType === 3011) {
5346
+ hasOle = true;
5347
+ }
5348
+ if (cIsContainer) sOff += 8;
5349
+ else sOff += 8 + cLen;
4621
5350
  }
4622
- offset += 8;
4623
- continue;
4624
- }
4625
- if (recType === 3998 && recLen > 0 && offset + 8 + recLen <= len) {
4626
- const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
4627
- const text = new TextDecoder("latin1").decode(bytes).trim();
4628
- if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
4629
- currentSlideTexts.push(text);
5351
+ let title = "";
5352
+ let subtitle = "";
5353
+ const paragraphs = [];
5354
+ const tableColumns = [];
5355
+ for (const t of rawTexts) {
5356
+ if (!title && t.length < 60 && !t.includes("\n")) {
5357
+ title = t;
5358
+ } else if (t.startsWith("Column ") || title === "Table" && t.startsWith("Column")) {
5359
+ tableColumns.push(t);
5360
+ } else if (t.length < 35 && (t.includes("#") || t.toUpperCase() === t) && !subtitle) {
5361
+ subtitle = t;
5362
+ } else {
5363
+ paragraphs.push(t);
5364
+ }
4630
5365
  }
4631
- }
4632
- if (recType === 3999 && recLen > 0 && offset + 8 + recLen <= len) {
4633
- const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
4634
- const text = new TextDecoder("utf-16le").decode(bytes).trim();
4635
- if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
4636
- currentSlideTexts.push(text);
5366
+ let pictureUrl = null;
5367
+ if ((hasOle || rawTexts.some((t) => t.toLowerCase().includes("chart") || t.toLowerCase().includes("figure"))) && picIdx < pictures.length) {
5368
+ pictureUrl = pictures[picIdx++];
4637
5369
  }
5370
+ slides.push({
5371
+ slideIndex: slides.length + 1,
5372
+ title: title || `Slide ${slides.length + 1}`,
5373
+ subtitle,
5374
+ paragraphs,
5375
+ tableColumns,
5376
+ pictureUrl,
5377
+ hasOle
5378
+ });
4638
5379
  }
4639
- const isContainer = (recVerInst & 15) === 15;
4640
- if (isContainer) {
4641
- offset += 8;
4642
- } else {
4643
- offset += 8 + recLen;
4644
- }
4645
- }
4646
- if (currentSlideTexts.length > 0) {
4647
- const title = currentSlideTexts[0] || "Slide";
4648
- const texts = currentSlideTexts.slice(1);
4649
- slides.push({ title, texts });
5380
+ if (isContainer) offset += 8;
5381
+ else offset += 8 + recLen;
4650
5382
  }
4651
5383
  if (slides.length === 0) {
4652
5384
  const rawText = new TextDecoder("latin1", { fatal: false }).decode(stream);
4653
- const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{4,}/g) || [];
4654
- const filtered = matches.map((m) => m.trim()).filter((m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times"));
4655
- if (filtered.length > 0) {
4656
- const chunkSize = 4;
4657
- for (let i = 0; i < filtered.length; i += chunkSize) {
4658
- const chunk = filtered.slice(i, i + chunkSize);
5385
+ const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{5,}/g) || [];
5386
+ const clean = matches.map((m) => m.trim()).filter(
5387
+ (m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times") && !m.includes("[Content_Types]") && !m.includes("_rels/") && !m.includes("xml")
5388
+ );
5389
+ if (clean.length > 0) {
5390
+ const chunkSize = 3;
5391
+ for (let i = 0; i < clean.length; i += chunkSize) {
5392
+ const chunk = clean.slice(i, i + chunkSize);
4659
5393
  slides.push({
5394
+ slideIndex: slides.length + 1,
4660
5395
  title: chunk[0] || `Slide ${Math.floor(i / chunkSize) + 1}`,
4661
- texts: chunk.slice(1)
5396
+ subtitle: chunk.length > 2 ? chunk[1] : void 0,
5397
+ paragraphs: chunk.length > 2 ? chunk.slice(2) : chunk.slice(1),
5398
+ tableColumns: [],
5399
+ pictureUrl: pictures[slides.length] || null
4662
5400
  });
4663
5401
  }
4664
5402
  }