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