@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.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Component, ChangeDetectionStrategy, Input, Output, ViewChild, EventEmitter as EventEmitter$1 } from '@angular/core';
2
2
  import { CommonModule } from '@angular/common';
3
3
  import DOMPurify2 from 'dompurify';
4
+ import * as pdfjsLib from 'pdfjs-dist';
4
5
  import * as docx from 'docx-preview';
5
6
  import { unzipSync, strFromU8, unzip } from 'fflate';
6
7
  import * as XLSX from 'xlsx';
@@ -11,7 +12,7 @@ import * as THREE from 'three';
11
12
  import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
12
13
  import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
13
14
  import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
14
- import { RTFJS } from 'rtf.js';
15
+ import * as RTFJS from 'rtf.js/dist/RTFJS.bundle.js';
15
16
 
16
17
  var __create = Object.create;
17
18
  var __defProp = Object.defineProperty;
@@ -314,6 +315,39 @@ function detectOoxmlType(buffer) {
314
315
  }
315
316
  return "application/zip";
316
317
  }
318
+ function detectCfbfType(buffer) {
319
+ const bytes = new Uint8Array(buffer);
320
+ const hasUtf16le = (str) => {
321
+ const target = new Uint8Array(str.length * 2);
322
+ for (let i = 0; i < str.length; i++) {
323
+ target[i * 2] = str.charCodeAt(i);
324
+ target[i * 2 + 1] = 0;
325
+ }
326
+ const targetLen = target.length;
327
+ const max = bytes.length - targetLen;
328
+ for (let i = 0; i <= max; i++) {
329
+ let match = true;
330
+ for (let j = 0; j < targetLen; j++) {
331
+ if (bytes[i + j] !== target[j]) {
332
+ match = false;
333
+ break;
334
+ }
335
+ }
336
+ if (match) return true;
337
+ }
338
+ return false;
339
+ };
340
+ if (hasUtf16le("PowerPoint Document")) {
341
+ return { mime: "application/vnd.ms-powerpoint", extension: ".ppt" };
342
+ }
343
+ if (hasUtf16le("WordDocument")) {
344
+ return { mime: "application/msword", extension: ".doc" };
345
+ }
346
+ if (hasUtf16le("Workbook") || hasUtf16le("Book")) {
347
+ return { mime: "application/vnd.ms-excel", extension: ".xls" };
348
+ }
349
+ return null;
350
+ }
317
351
  function extractExtension(nameOrUrl) {
318
352
  try {
319
353
  const url = new URL(nameOrUrl);
@@ -378,6 +412,18 @@ async function sourceToArrayBuffer(source, signal) {
378
412
  } else {
379
413
  metadata.mimeType = metadata.mimeType ?? magicMime;
380
414
  }
415
+ } else if (magicMime === "application/x-cfbf") {
416
+ if (metadata.extension) {
417
+ metadata.mimeType = mimeFromExtension(metadata.extension) ?? magicMime;
418
+ } else {
419
+ const cfbf = detectCfbfType(buffer);
420
+ if (cfbf) {
421
+ metadata.mimeType = cfbf.mime;
422
+ metadata.extension = cfbf.extension;
423
+ } else {
424
+ metadata.mimeType = magicMime;
425
+ }
426
+ }
381
427
  } else {
382
428
  metadata.mimeType = magicMime;
383
429
  }
@@ -482,11 +528,21 @@ var ToolbarController = class {
482
528
  el;
483
529
  toolbarEl;
484
530
  actions = [];
531
+ pageInputEl = null;
532
+ pageLabelEl = null;
485
533
  constructor(container) {
486
534
  this.el = container;
487
535
  this.toolbarEl = createElement("div", { className: "fp-toolbar" });
488
536
  this.el.appendChild(this.toolbarEl);
489
537
  }
538
+ setPage(page, max) {
539
+ if (this.pageInputEl) {
540
+ this.pageInputEl.value = page.toString();
541
+ }
542
+ if (max !== void 0 && this.pageLabelEl) {
543
+ this.pageLabelEl.textContent = ` / ${max}`;
544
+ }
545
+ }
490
546
  update(actions) {
491
547
  this.actions = actions;
492
548
  this.render();
@@ -529,6 +585,7 @@ var ToolbarController = class {
529
585
  if (action.type === "separator") {
530
586
  groupEl.appendChild(createElement("div", { className: "fp-toolbar-separator" }));
531
587
  } else if (action.type === "page-nav") {
588
+ const max = action.max ?? 1;
532
589
  const prevBtn = this.createButton(
533
590
  "prev",
534
591
  ICON_PAGE_PREV,
@@ -547,7 +604,6 @@ var ToolbarController = class {
547
604
  "Next Page",
548
605
  () => {
549
606
  const cur = parseInt(input.value, 10) || 1;
550
- const max = action.max ?? 1;
551
607
  if (cur < max) {
552
608
  input.value = (cur + 1).toString();
553
609
  action.execute("next", cur + 1);
@@ -559,15 +615,18 @@ var ToolbarController = class {
559
615
  type: "number",
560
616
  value: (action.value ?? 1).toString(),
561
617
  min: "1",
562
- max: (action.max ?? 1).toString()
618
+ max: max.toString()
563
619
  });
564
620
  input.addEventListener("change", () => {
565
- const val = parseInt(input.value, 10);
566
- if (!isNaN(val)) {
567
- action.execute("go", val);
568
- }
621
+ let val = parseInt(input.value, 10);
622
+ if (isNaN(val)) val = 1;
623
+ val = Math.max(1, Math.min(max, val));
624
+ input.value = val.toString();
625
+ action.execute("go", val);
569
626
  });
570
- const label = createElement("span", { className: "fp-toolbar-label" }, ` / ${action.max ?? 1}`);
627
+ const label = createElement("span", { className: "fp-toolbar-label" }, ` / ${max}`);
628
+ this.pageInputEl = input;
629
+ this.pageLabelEl = label;
571
630
  groupEl.appendChild(prevBtn);
572
631
  groupEl.appendChild(input);
573
632
  groupEl.appendChild(label);
@@ -745,7 +804,13 @@ var FilePreviewViewer = class {
745
804
  buffer,
746
805
  options,
747
806
  signal,
748
- emit: (event, payload) => this.eventEmitter.emit(event, payload)
807
+ emit: (event, payload) => {
808
+ if (event === "page-change" && payload && typeof payload.page === "number") {
809
+ const total = payload.total ?? payload.totalPages;
810
+ this.toolbar?.setPage(payload.page, total);
811
+ }
812
+ this.eventEmitter.emit(event, payload);
813
+ }
749
814
  });
750
815
  this.activeInstance = instance;
751
816
  this.hideLoading();
@@ -779,6 +844,7 @@ var FilePreviewViewer = class {
779
844
  if (thumbnails && thumbnails.length > 0 && this.thumbnailPanel) {
780
845
  this.thumbnailPanel.update(thumbnails, (index) => {
781
846
  instance.goToPage?.(index + 1);
847
+ this.toolbar?.setPage(index + 1);
782
848
  });
783
849
  if (options.showThumbnails) {
784
850
  this.thumbnailPanel.show();
@@ -904,9 +970,13 @@ var FilePreviewViewer = class {
904
970
  if (e.key === "ArrowRight" || e.key === "PageDown") {
905
971
  const cur = this.activeInstance.getCurrentPage?.() ?? 1;
906
972
  this.activeInstance.goToPage?.(cur + 1);
973
+ const nextCur = this.activeInstance.getCurrentPage?.() ?? cur + 1;
974
+ this.toolbar?.setPage(nextCur);
907
975
  } else if (e.key === "ArrowLeft" || e.key === "PageUp") {
908
976
  const cur = this.activeInstance.getCurrentPage?.() ?? 1;
909
977
  this.activeInstance.goToPage?.(Math.max(1, cur - 1));
978
+ const prevCur = this.activeInstance.getCurrentPage?.() ?? Math.max(1, cur - 1);
979
+ this.toolbar?.setPage(prevCur);
910
980
  } else if (e.key === "+" || e.key === "=") {
911
981
  this.activeInstance.zoomIn?.();
912
982
  } else if (e.key === "-" || e.key === "_") {
@@ -1172,30 +1242,62 @@ var CfbfReader = class {
1172
1242
  return result.subarray(0, targetSize);
1173
1243
  }
1174
1244
  };
1175
-
1176
- // ../plugins/pdf/dist/index.js
1245
+ if (typeof window !== "undefined" && pdfjsLib.GlobalWorkerOptions) {
1246
+ if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
1247
+ pdfjsLib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/build/pdf.worker.min.mjs`;
1248
+ }
1249
+ }
1177
1250
  var PdfPlugin = class {
1178
1251
  id = "pdf";
1179
- name = "PDF Preview";
1252
+ name = "PDF Document Preview";
1180
1253
  extensions = [".pdf"];
1181
1254
  mimeTypes = ["application/pdf"];
1182
1255
  weight = 100;
1183
1256
  supports(file) {
1184
1257
  const ext = file.metadata.extension?.toLowerCase();
1185
1258
  const mime = file.metadata.mimeType?.toLowerCase();
1186
- return ext === ".pdf" || mime === "application/pdf";
1259
+ if (ext) return this.extensions.includes(ext);
1260
+ return this.mimeTypes.includes(mime || "");
1187
1261
  }
1188
1262
  getToolbarActions(instance) {
1263
+ const totalPages = instance.getPageCount?.() ?? 1;
1264
+ const curPage = instance.getCurrentPage?.() ?? 1;
1189
1265
  return [
1266
+ {
1267
+ id: "thumbnails",
1268
+ icon: "thumbnails",
1269
+ label: "Page Thumbnails",
1270
+ type: "button",
1271
+ group: "navigation",
1272
+ execute: () => instance.toggleThumbnails?.()
1273
+ },
1274
+ {
1275
+ id: "page-nav",
1276
+ icon: "",
1277
+ label: "Page Navigation",
1278
+ type: "page-nav",
1279
+ group: "navigation",
1280
+ value: curPage,
1281
+ max: totalPages,
1282
+ execute: (action, page) => {
1283
+ const cur = instance.getCurrentPage?.() ?? 1;
1284
+ const max = instance.getPageCount?.() ?? 1;
1285
+ if (action === "prev") {
1286
+ if (cur > 1) instance.goToPage?.(cur - 1);
1287
+ } else if (action === "next") {
1288
+ if (cur < max) instance.goToPage?.(cur + 1);
1289
+ } else if (typeof page === "number") {
1290
+ instance.goToPage?.(page);
1291
+ }
1292
+ }
1293
+ },
1190
1294
  {
1191
1295
  id: "zoom-out",
1192
1296
  icon: "zoom-out",
1193
1297
  label: "Zoom Out",
1194
1298
  type: "button",
1195
1299
  group: "zoom",
1196
- execute: () => {
1197
- instance.zoomOut?.();
1198
- }
1300
+ execute: () => instance.zoomOut?.()
1199
1301
  },
1200
1302
  {
1201
1303
  id: "zoom-in",
@@ -1203,9 +1305,7 @@ var PdfPlugin = class {
1203
1305
  label: "Zoom In",
1204
1306
  type: "button",
1205
1307
  group: "zoom",
1206
- execute: () => {
1207
- instance.zoomIn?.();
1208
- }
1308
+ execute: () => instance.zoomIn?.()
1209
1309
  },
1210
1310
  {
1211
1311
  id: "fit-page",
@@ -1213,39 +1313,23 @@ var PdfPlugin = class {
1213
1313
  label: "Fit to Page",
1214
1314
  type: "button",
1215
1315
  group: "zoom",
1216
- execute: () => {
1217
- instance.fitToPage?.();
1218
- }
1316
+ execute: () => instance.fitToPage?.()
1219
1317
  },
1220
1318
  {
1221
1319
  id: "rotate-cw",
1222
1320
  icon: "rotate-cw",
1223
- label: "Rotate",
1321
+ label: "Rotate Clockwise",
1224
1322
  type: "button",
1225
1323
  group: "view",
1226
- execute: () => {
1227
- instance.rotateCW?.();
1228
- }
1229
- },
1230
- {
1231
- id: "page-nav",
1232
- icon: "page-nav",
1233
- label: "Page Navigation",
1234
- type: "page-nav",
1235
- group: "navigation",
1236
- execute: (page) => {
1237
- if (typeof page === "number") instance.goToPage?.(page);
1238
- }
1324
+ execute: () => instance.rotateCW?.()
1239
1325
  },
1240
1326
  {
1241
1327
  id: "download",
1242
1328
  icon: "download",
1243
- label: "Download",
1329
+ label: "Download PDF",
1244
1330
  type: "button",
1245
1331
  group: "actions",
1246
- execute: () => {
1247
- instance.download?.();
1248
- }
1332
+ execute: () => instance.download?.()
1249
1333
  },
1250
1334
  {
1251
1335
  id: "print",
@@ -1253,99 +1337,213 @@ var PdfPlugin = class {
1253
1337
  label: "Print",
1254
1338
  type: "button",
1255
1339
  group: "actions",
1256
- execute: () => {
1257
- instance.print?.();
1258
- }
1340
+ execute: () => instance.print?.()
1259
1341
  }
1260
1342
  ];
1261
1343
  }
1262
1344
  async render(ctx) {
1263
- const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1264
- const url = URL.createObjectURL(blob);
1265
- const wrapper = document.createElement("div");
1266
- wrapper.style.width = "100%";
1267
- wrapper.style.height = "100%";
1268
- wrapper.style.overflow = "hidden";
1269
- wrapper.style.display = "flex";
1270
- wrapper.style.justifyContent = "center";
1271
- wrapper.style.alignItems = "center";
1272
- const iframe = document.createElement("iframe");
1273
- iframe.src = url;
1274
- iframe.style.width = "100%";
1275
- iframe.style.height = "100%";
1276
- iframe.style.border = "none";
1277
- wrapper.appendChild(iframe);
1278
- ctx.container.appendChild(wrapper);
1345
+ const container = document.createElement("div");
1346
+ container.className = "fp-pdf-container";
1347
+ container.style.width = "100%";
1348
+ container.style.height = "100%";
1349
+ container.style.overflow = "auto";
1350
+ container.style.display = "flex";
1351
+ container.style.flexDirection = "column";
1352
+ container.style.alignItems = "center";
1353
+ container.style.padding = "24px 16px";
1354
+ container.style.backgroundColor = "#0f172a";
1355
+ container.style.boxSizing = "border-box";
1356
+ container.style.position = "relative";
1357
+ const pageCard = document.createElement("div");
1358
+ pageCard.className = "fp-pdf-page-card";
1359
+ pageCard.style.boxShadow = "0 10px 35px rgba(0, 0, 0, 0.5)";
1360
+ pageCard.style.backgroundColor = "#ffffff";
1361
+ pageCard.style.borderRadius = "4px";
1362
+ pageCard.style.overflow = "hidden";
1363
+ pageCard.style.lineHeight = "0";
1364
+ pageCard.style.transition = "transform 0.15s ease";
1365
+ pageCard.style.position = "relative";
1366
+ const canvas = document.createElement("canvas");
1367
+ pageCard.appendChild(canvas);
1368
+ container.appendChild(pageCard);
1369
+ const indicator = document.createElement("div");
1370
+ indicator.className = "fp-pdf-page-indicator";
1371
+ indicator.style.position = "sticky";
1372
+ indicator.style.bottom = "16px";
1373
+ indicator.style.marginTop = "16px";
1374
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
1375
+ indicator.style.backdropFilter = "blur(8px)";
1376
+ indicator.style.color = "#f8fafc";
1377
+ indicator.style.fontSize = "12px";
1378
+ indicator.style.fontWeight = "600";
1379
+ indicator.style.padding = "5px 14px";
1380
+ indicator.style.borderRadius = "20px";
1381
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
1382
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
1383
+ indicator.style.zIndex = "10";
1384
+ indicator.style.userSelect = "none";
1385
+ indicator.style.pointerEvents = "none";
1386
+ container.appendChild(indicator);
1387
+ ctx.container.appendChild(container);
1388
+ const loadingTask = pdfjsLib.getDocument({
1389
+ data: new Uint8Array(ctx.buffer),
1390
+ cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/cmaps/`,
1391
+ cMapPacked: true,
1392
+ standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/standard_fonts/`
1393
+ });
1394
+ const pdfDoc = await loadingTask.promise;
1395
+ const totalPages = Math.max(1, pdfDoc.numPages);
1279
1396
  let currentPage = 1;
1280
- let currentZoom = 1;
1397
+ let zoomScale = 1;
1281
1398
  let rotation = 0;
1399
+ let currentRenderTask = null;
1400
+ const renderPage = async (pageNum) => {
1401
+ if (currentRenderTask) {
1402
+ try {
1403
+ currentRenderTask.cancel();
1404
+ } catch {
1405
+ }
1406
+ currentRenderTask = null;
1407
+ }
1408
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
1409
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
1410
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
1411
+ const page = await pdfDoc.getPage(currentPage);
1412
+ const containerWidth = container.clientWidth || 900;
1413
+ const unscaledVp = page.getViewport({ scale: 1, rotation });
1414
+ const baseScale = Math.min((containerWidth - 64) / unscaledVp.width, 1.6);
1415
+ const effectiveScale = (baseScale > 0 ? baseScale : 1) * zoomScale;
1416
+ const pixelRatio = window.devicePixelRatio || 1;
1417
+ const viewport = page.getViewport({ scale: effectiveScale, rotation });
1418
+ canvas.width = Math.floor(viewport.width * pixelRatio);
1419
+ canvas.height = Math.floor(viewport.height * pixelRatio);
1420
+ canvas.style.width = `${Math.floor(viewport.width)}px`;
1421
+ canvas.style.height = `${Math.floor(viewport.height)}px`;
1422
+ const canvasCtx = canvas.getContext("2d");
1423
+ if (!canvasCtx) return;
1424
+ canvasCtx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
1425
+ currentRenderTask = page.render({
1426
+ canvasContext: canvasCtx,
1427
+ viewport
1428
+ });
1429
+ try {
1430
+ await currentRenderTask.promise;
1431
+ } catch (err) {
1432
+ if (err?.name !== "RenderingCancelledException") {
1433
+ console.warn("[PdfPlugin] Page render warning:", err);
1434
+ }
1435
+ } finally {
1436
+ currentRenderTask = null;
1437
+ }
1438
+ };
1439
+ await renderPage(1);
1282
1440
  const cleanup = () => {
1283
- URL.revokeObjectURL(url);
1284
- wrapper.remove();
1441
+ if (currentRenderTask) {
1442
+ try {
1443
+ currentRenderTask.cancel();
1444
+ } catch {
1445
+ }
1446
+ }
1447
+ try {
1448
+ pdfDoc.destroy();
1449
+ } catch {
1450
+ }
1451
+ container.remove();
1285
1452
  ctx.container.innerHTML = "";
1286
1453
  };
1287
1454
  ctx.signal.addEventListener("abort", cleanup);
1288
- return {
1455
+ const instance = {
1289
1456
  destroy: cleanup,
1290
1457
  zoomIn: () => {
1291
- currentZoom += 0.1;
1292
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1458
+ zoomScale = Math.min(3.5, zoomScale + 0.2);
1459
+ renderPage(currentPage);
1293
1460
  },
1294
1461
  zoomOut: () => {
1295
- currentZoom = Math.max(0.2, currentZoom - 0.1);
1296
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1462
+ zoomScale = Math.max(0.3, zoomScale - 0.2);
1463
+ renderPage(currentPage);
1297
1464
  },
1298
- getZoom: () => currentZoom,
1465
+ getZoom: () => zoomScale,
1299
1466
  setZoom: (level) => {
1300
- currentZoom = level;
1301
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1467
+ zoomScale = Math.max(0.3, Math.min(3.5, level));
1468
+ renderPage(currentPage);
1302
1469
  },
1303
1470
  fitToPage: () => {
1304
- currentZoom = 1;
1305
- iframe.style.transform = `scale(1) rotate(${rotation}deg)`;
1471
+ zoomScale = 1;
1472
+ renderPage(currentPage);
1306
1473
  },
1307
1474
  rotateCW: () => {
1308
1475
  rotation = (rotation + 90) % 360;
1309
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1476
+ renderPage(currentPage);
1310
1477
  },
1311
1478
  rotateCCW: () => {
1312
1479
  rotation = (rotation - 90 + 360) % 360;
1313
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1480
+ renderPage(currentPage);
1314
1481
  },
1315
1482
  getRotation: () => rotation,
1483
+ getPageCount: () => totalPages,
1484
+ getCurrentPage: () => currentPage,
1316
1485
  goToPage: (page) => {
1317
- currentPage = page;
1318
- iframe.src = `${url}#page=${page}`;
1486
+ renderPage(page);
1319
1487
  },
1320
- getCurrentPage: () => currentPage,
1321
1488
  download: () => {
1489
+ const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1490
+ const url = URL.createObjectURL(blob);
1322
1491
  const a = document.createElement("a");
1323
1492
  a.href = url;
1324
1493
  a.download = ctx.metadata.name || "document.pdf";
1325
1494
  a.click();
1495
+ URL.revokeObjectURL(url);
1326
1496
  },
1327
1497
  print: () => {
1328
- iframe.contentWindow?.print();
1498
+ const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1499
+ const url = URL.createObjectURL(blob);
1500
+ const hiddenIframe = document.createElement("iframe");
1501
+ hiddenIframe.style.position = "fixed";
1502
+ hiddenIframe.style.right = "0";
1503
+ hiddenIframe.style.bottom = "0";
1504
+ hiddenIframe.style.width = "0";
1505
+ hiddenIframe.style.height = "0";
1506
+ hiddenIframe.style.border = "0";
1507
+ document.body.appendChild(hiddenIframe);
1508
+ hiddenIframe.src = url;
1509
+ hiddenIframe.onload = () => {
1510
+ setTimeout(() => {
1511
+ hiddenIframe.contentWindow?.print();
1512
+ setTimeout(() => {
1513
+ hiddenIframe.remove();
1514
+ URL.revokeObjectURL(url);
1515
+ }, 1e3);
1516
+ }, 300);
1517
+ };
1329
1518
  },
1330
1519
  getThumbnails: async () => {
1331
- return [
1332
- {
1333
- index: 1,
1334
- label: "Page 1",
1335
- render: async (canvas) => {
1336
- const context = canvas.getContext("2d");
1337
- if (context) {
1338
- context.fillStyle = "#fff";
1339
- context.fillRect(0, 0, canvas.width, canvas.height);
1340
- context.fillStyle = "#333";
1341
- context.font = "12px sans-serif";
1342
- context.fillText("PDF Preview", 10, 20);
1520
+ const thumbnails = [];
1521
+ const count = Math.min(totalPages, 50);
1522
+ for (let i = 1; i <= count; i++) {
1523
+ thumbnails.push({
1524
+ index: i,
1525
+ label: `Page ${i}`,
1526
+ render: async (thumbCanvas) => {
1527
+ try {
1528
+ const p = await pdfDoc.getPage(i);
1529
+ const baseVp = p.getViewport({ scale: 1 });
1530
+ const thumbScale = (thumbCanvas.width || 120) / baseVp.width;
1531
+ const thumbVp = p.getViewport({ scale: thumbScale });
1532
+ thumbCanvas.height = Math.floor(thumbVp.height);
1533
+ const tCtx = thumbCanvas.getContext("2d");
1534
+ if (tCtx) {
1535
+ await p.render({ canvasContext: tCtx, viewport: thumbVp }).promise;
1536
+ }
1537
+ } catch (e) {
1538
+ console.warn(`[PdfPlugin] Error generating thumbnail for page ${i}:`, e);
1343
1539
  }
1344
1540
  }
1345
- }
1346
- ];
1541
+ });
1542
+ }
1543
+ return thumbnails;
1347
1544
  }
1348
1545
  };
1546
+ return instance;
1349
1547
  }
1350
1548
  };
1351
1549
  function pdfPlugin() {
@@ -1666,10 +1864,37 @@ var DocxPlugin = class {
1666
1864
  supports(file) {
1667
1865
  const ext = file.metadata.extension?.toLowerCase();
1668
1866
  const mime = file.metadata.mimeType?.toLowerCase();
1669
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
1867
+ if (ext) {
1868
+ return this.extensions.includes(ext);
1869
+ }
1870
+ return this.mimeTypes.includes(mime || "");
1670
1871
  }
1671
1872
  getToolbarActions(instance) {
1672
- return [
1873
+ const totalPages = instance.getPageCount?.() ?? 1;
1874
+ const actions = [];
1875
+ if (totalPages > 1) {
1876
+ actions.push({
1877
+ id: "page-nav",
1878
+ icon: "",
1879
+ label: "Page Navigation",
1880
+ type: "page-nav",
1881
+ group: "navigation",
1882
+ value: instance.getCurrentPage?.() ?? 1,
1883
+ max: totalPages,
1884
+ execute: (action, page) => {
1885
+ const cur = instance.getCurrentPage?.() ?? 1;
1886
+ const max = instance.getPageCount?.() ?? 1;
1887
+ if (action === "prev") {
1888
+ if (cur > 1) instance.goToPage?.(cur - 1);
1889
+ } else if (action === "next") {
1890
+ if (cur < max) instance.goToPage?.(cur + 1);
1891
+ } else if (typeof page === "number") {
1892
+ instance.goToPage?.(page);
1893
+ }
1894
+ }
1895
+ });
1896
+ }
1897
+ actions.push(
1673
1898
  {
1674
1899
  id: "zoom-out",
1675
1900
  icon: "zoom-out",
@@ -1710,7 +1935,8 @@ var DocxPlugin = class {
1710
1935
  group: "actions",
1711
1936
  execute: () => instance.print?.()
1712
1937
  }
1713
- ];
1938
+ );
1939
+ return actions;
1714
1940
  }
1715
1941
  async render(ctx) {
1716
1942
  const wrapper = document.createElement("div");
@@ -1786,7 +2012,51 @@ var DocxPlugin = class {
1786
2012
  }
1787
2013
  }
1788
2014
  }
2015
+ const sections = wrapper.querySelectorAll("section.docx");
2016
+ const cards = wrapper.querySelectorAll(".fp-docx-page-card");
2017
+ const pageElements = sections.length > 0 ? sections : cards;
2018
+ const totalPages = Math.max(1, pageElements.length);
2019
+ let currentPage = 1;
2020
+ let indicator = null;
2021
+ if (totalPages > 1) {
2022
+ indicator = document.createElement("div");
2023
+ indicator.className = "fp-docx-page-indicator";
2024
+ indicator.style.position = "sticky";
2025
+ indicator.style.bottom = "16px";
2026
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
2027
+ indicator.style.backdropFilter = "blur(8px)";
2028
+ indicator.style.color = "#f8fafc";
2029
+ indicator.style.fontSize = "12px";
2030
+ indicator.style.fontWeight = "600";
2031
+ indicator.style.padding = "5px 14px";
2032
+ indicator.style.borderRadius = "20px";
2033
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
2034
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
2035
+ indicator.style.zIndex = "10";
2036
+ indicator.style.userSelect = "none";
2037
+ indicator.style.pointerEvents = "none";
2038
+ indicator.style.textAlign = "center";
2039
+ indicator.style.width = "fit-content";
2040
+ indicator.style.margin = "16px auto 0";
2041
+ ctx.container.appendChild(indicator);
2042
+ }
2043
+ const showPage = (pageNum) => {
2044
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
2045
+ if (pageElements.length > 1) {
2046
+ pageElements.forEach((sec, idx) => {
2047
+ sec.style.display = idx + 1 === currentPage ? "block" : "none";
2048
+ });
2049
+ }
2050
+ if (indicator) {
2051
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
2052
+ }
2053
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
2054
+ };
2055
+ if (totalPages > 1) {
2056
+ showPage(1);
2057
+ }
1789
2058
  const cleanup = () => {
2059
+ indicator?.remove();
1790
2060
  for (const url of createdBlobUrls) {
1791
2061
  URL.revokeObjectURL(url);
1792
2062
  }
@@ -1797,6 +2067,11 @@ var DocxPlugin = class {
1797
2067
  ctx.signal.addEventListener("abort", cleanup);
1798
2068
  return {
1799
2069
  destroy: cleanup,
2070
+ getPageCount: () => totalPages,
2071
+ getCurrentPage: () => currentPage,
2072
+ goToPage: (page) => {
2073
+ showPage(page);
2074
+ },
1800
2075
  zoomIn: () => {
1801
2076
  scale += 0.1;
1802
2077
  wrapper.style.transform = `scale(${scale})`;
@@ -2055,10 +2330,37 @@ var ExcelPlugin = class {
2055
2330
  supports(file) {
2056
2331
  const ext = file.metadata.extension?.toLowerCase();
2057
2332
  const mime = file.metadata.mimeType?.toLowerCase();
2058
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
2333
+ if (ext) {
2334
+ return this.extensions.includes(ext);
2335
+ }
2336
+ return this.mimeTypes.includes(mime || "");
2059
2337
  }
2060
2338
  getToolbarActions(instance) {
2061
- return [
2339
+ const totalSheets = instance.getPageCount?.() ?? 1;
2340
+ const actions = [];
2341
+ if (totalSheets > 1) {
2342
+ actions.push({
2343
+ id: "page-nav",
2344
+ icon: "",
2345
+ label: "Sheet Navigation",
2346
+ type: "page-nav",
2347
+ group: "navigation",
2348
+ value: instance.getCurrentPage?.() ?? 1,
2349
+ max: totalSheets,
2350
+ execute: (action, sheet) => {
2351
+ const cur = instance.getCurrentPage?.() ?? 1;
2352
+ const max = instance.getPageCount?.() ?? 1;
2353
+ if (action === "prev") {
2354
+ if (cur > 1) instance.goToPage?.(cur - 1);
2355
+ } else if (action === "next") {
2356
+ if (cur < max) instance.goToPage?.(cur + 1);
2357
+ } else if (typeof sheet === "number") {
2358
+ instance.goToPage?.(sheet);
2359
+ }
2360
+ }
2361
+ });
2362
+ }
2363
+ actions.push(
2062
2364
  {
2063
2365
  id: "zoom-out",
2064
2366
  icon: "zoom-out",
@@ -2079,16 +2381,6 @@ var ExcelPlugin = class {
2079
2381
  instance.zoomIn?.();
2080
2382
  }
2081
2383
  },
2082
- {
2083
- id: "page-nav",
2084
- icon: "page-nav",
2085
- label: "Sheet Navigation",
2086
- type: "page-nav",
2087
- group: "navigation",
2088
- execute: (sheet) => {
2089
- if (typeof sheet === "number") instance.goToPage?.(sheet);
2090
- }
2091
- },
2092
2384
  {
2093
2385
  id: "download",
2094
2386
  icon: "download",
@@ -2109,7 +2401,8 @@ var ExcelPlugin = class {
2109
2401
  instance.print?.();
2110
2402
  }
2111
2403
  }
2112
- ];
2404
+ );
2405
+ return actions;
2113
2406
  }
2114
2407
  async render(ctx) {
2115
2408
  const container = document.createElement("div");
@@ -2193,6 +2486,7 @@ var ExcelPlugin = class {
2193
2486
  b.style.fontWeight = "normal";
2194
2487
  }
2195
2488
  });
2489
+ ctx.emit("page-change", { page: currentSheetIndex, total: sheetNames.length });
2196
2490
  };
2197
2491
  if (sheetNames.length > 0) {
2198
2492
  sheetNames.forEach((name, idx) => {
@@ -2442,7 +2736,31 @@ var CodePlugin = class {
2442
2736
  return false;
2443
2737
  }
2444
2738
  getToolbarActions(instance) {
2445
- return [
2739
+ const totalPages = instance.getPageCount?.() ?? 1;
2740
+ const actions = [];
2741
+ if (totalPages > 1) {
2742
+ actions.push({
2743
+ id: "page-nav",
2744
+ icon: "",
2745
+ label: "Page Navigation",
2746
+ type: "page-nav",
2747
+ group: "navigation",
2748
+ value: instance.getCurrentPage?.() ?? 1,
2749
+ max: totalPages,
2750
+ execute: (action, page) => {
2751
+ const cur = instance.getCurrentPage?.() ?? 1;
2752
+ const max = instance.getPageCount?.() ?? 1;
2753
+ if (action === "prev") {
2754
+ if (cur > 1) instance.goToPage?.(cur - 1);
2755
+ } else if (action === "next") {
2756
+ if (cur < max) instance.goToPage?.(cur + 1);
2757
+ } else if (typeof page === "number") {
2758
+ instance.goToPage?.(page);
2759
+ }
2760
+ }
2761
+ });
2762
+ }
2763
+ actions.push(
2446
2764
  {
2447
2765
  id: "zoom-out",
2448
2766
  icon: "zoom-out",
@@ -2493,11 +2811,16 @@ var CodePlugin = class {
2493
2811
  instance.print?.();
2494
2812
  }
2495
2813
  }
2496
- ];
2814
+ );
2815
+ return actions;
2497
2816
  }
2498
2817
  async render(ctx) {
2499
2818
  const decoder = new TextDecoder("utf-8");
2500
- const text = decoder.decode(ctx.buffer);
2819
+ const fullText = decoder.decode(ctx.buffer);
2820
+ const pageSplitRegex = /(?:\f|\x0C|(?:\r?\n|^)\s*[-=_]{3,}\s*(?:PAGE|Page|page break|Page Break)[\s\d\w-]*[-=_]{3,}\s*(?:\r?\n|$))/i;
2821
+ const rawPages = fullText.split(pageSplitRegex).map((p) => p.trim()).filter((p) => p.length > 0);
2822
+ const totalPages = Math.max(1, rawPages.length);
2823
+ let currentPage = 1;
2501
2824
  const container = document.createElement("div");
2502
2825
  container.style.width = "100%";
2503
2826
  container.style.height = "100%";
@@ -2516,25 +2839,66 @@ var CodePlugin = class {
2516
2839
  pre.style.wordBreak = "break-all";
2517
2840
  const code = document.createElement("code");
2518
2841
  const ext = (ctx.metadata.extension || "").replace(".", "");
2519
- try {
2520
- if (ext && hljs.getLanguage(ext)) {
2521
- code.innerHTML = hljs.highlight(text, { language: ext }).value;
2522
- } else {
2523
- code.innerHTML = hljs.highlightAuto(text).value;
2842
+ const renderCodePage = (text) => {
2843
+ try {
2844
+ if (ext && hljs.getLanguage(ext)) {
2845
+ code.innerHTML = hljs.highlight(text, { language: ext }).value;
2846
+ } else {
2847
+ code.innerHTML = hljs.highlightAuto(text).value;
2848
+ }
2849
+ } catch {
2850
+ code.textContent = text;
2524
2851
  }
2525
- } catch {
2526
- code.textContent = text;
2527
- }
2852
+ };
2853
+ renderCodePage(rawPages[0] || fullText);
2528
2854
  pre.appendChild(code);
2529
2855
  container.appendChild(pre);
2856
+ let indicator = null;
2857
+ if (totalPages > 1) {
2858
+ indicator = document.createElement("div");
2859
+ indicator.className = "fp-code-page-indicator";
2860
+ indicator.style.position = "sticky";
2861
+ indicator.style.bottom = "16px";
2862
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
2863
+ indicator.style.backdropFilter = "blur(8px)";
2864
+ indicator.style.color = "#f8fafc";
2865
+ indicator.style.fontSize = "12px";
2866
+ indicator.style.fontWeight = "600";
2867
+ indicator.style.padding = "5px 14px";
2868
+ indicator.style.borderRadius = "20px";
2869
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
2870
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
2871
+ indicator.style.zIndex = "10";
2872
+ indicator.style.userSelect = "none";
2873
+ indicator.style.pointerEvents = "none";
2874
+ indicator.style.textAlign = "center";
2875
+ indicator.style.width = "fit-content";
2876
+ indicator.style.margin = "16px auto 0";
2877
+ container.appendChild(indicator);
2878
+ }
2879
+ const showPage = (pageNum) => {
2880
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
2881
+ renderCodePage(rawPages[currentPage - 1] || fullText);
2882
+ if (indicator) {
2883
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
2884
+ }
2885
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
2886
+ };
2887
+ if (totalPages > 1) {
2888
+ showPage(1);
2889
+ }
2530
2890
  ctx.container.appendChild(container);
2531
2891
  const cleanup = () => {
2892
+ indicator?.remove();
2532
2893
  container.remove();
2533
2894
  ctx.container.innerHTML = "";
2534
2895
  };
2535
2896
  ctx.signal.addEventListener("abort", cleanup);
2536
2897
  return {
2537
2898
  destroy: cleanup,
2899
+ getPageCount: () => totalPages,
2900
+ getCurrentPage: () => currentPage,
2901
+ goToPage: (page) => showPage(page),
2538
2902
  zoomIn: () => {
2539
2903
  fontSize = Math.min(32, fontSize + 2);
2540
2904
  pre.style.fontSize = `${fontSize}px`;
@@ -2562,7 +2926,7 @@ var CodePlugin = class {
2562
2926
  window.print();
2563
2927
  },
2564
2928
  copy: () => {
2565
- navigator.clipboard?.writeText(text);
2929
+ navigator.clipboard?.writeText(fullText);
2566
2930
  }
2567
2931
  };
2568
2932
  }
@@ -2997,7 +3361,10 @@ var PptxPlugin = class {
2997
3361
  supports(file) {
2998
3362
  const ext = file.metadata.extension?.toLowerCase();
2999
3363
  const mime = file.metadata.mimeType?.toLowerCase();
3000
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3364
+ if (ext) {
3365
+ return this.extensions.includes(ext);
3366
+ }
3367
+ return this.mimeTypes.includes(mime || "");
3001
3368
  }
3002
3369
  getToolbarActions(instance) {
3003
3370
  return [
@@ -3381,7 +3748,31 @@ var RtfPlugin = class {
3381
3748
  return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3382
3749
  }
3383
3750
  getToolbarActions(instance) {
3384
- return [
3751
+ const totalPages = instance.getPageCount?.() ?? 1;
3752
+ const actions = [];
3753
+ if (totalPages > 1) {
3754
+ actions.push({
3755
+ id: "page-nav",
3756
+ icon: "",
3757
+ label: "Page Navigation",
3758
+ type: "page-nav",
3759
+ group: "navigation",
3760
+ value: instance.getCurrentPage?.() ?? 1,
3761
+ max: totalPages,
3762
+ execute: (action, page) => {
3763
+ const cur = instance.getCurrentPage?.() ?? 1;
3764
+ const max = instance.getPageCount?.() ?? 1;
3765
+ if (action === "prev") {
3766
+ if (cur > 1) instance.goToPage?.(cur - 1);
3767
+ } else if (action === "next") {
3768
+ if (cur < max) instance.goToPage?.(cur + 1);
3769
+ } else if (typeof page === "number") {
3770
+ instance.goToPage?.(page);
3771
+ }
3772
+ }
3773
+ });
3774
+ }
3775
+ actions.push(
3385
3776
  {
3386
3777
  id: "zoom-out",
3387
3778
  icon: "zoom-out",
@@ -3422,7 +3813,8 @@ var RtfPlugin = class {
3422
3813
  group: "actions",
3423
3814
  execute: () => instance.print?.()
3424
3815
  }
3425
- ];
3816
+ );
3817
+ return actions;
3426
3818
  }
3427
3819
  async render(ctx) {
3428
3820
  const wrapper = document.createElement("div");
@@ -3441,25 +3833,82 @@ var RtfPlugin = class {
3441
3833
  ctx.container.style.backgroundColor = "#f1f5f9";
3442
3834
  ctx.container.appendChild(wrapper);
3443
3835
  let scale = 1;
3836
+ let pageElements = [];
3444
3837
  try {
3838
+ if (typeof RTFJS.loggingEnabled === "function") {
3839
+ RTFJS.loggingEnabled(false);
3840
+ }
3445
3841
  const doc = new RTFJS.Document(ctx.buffer, {});
3446
3842
  const htmlElements = await doc.render();
3447
- for (const el of htmlElements) {
3843
+ pageElements = htmlElements;
3844
+ for (let i = 0; i < htmlElements.length; i++) {
3845
+ const el = htmlElements[i];
3846
+ el.style.display = i === 0 ? "block" : "none";
3448
3847
  wrapper.appendChild(el);
3449
3848
  }
3450
3849
  } catch (err) {
3451
3850
  console.warn("[RtfPlugin] RTF render error, fallback text:", err);
3452
3851
  const text = new TextDecoder("latin1").decode(ctx.buffer);
3453
3852
  const clean = text.replace(/\\par[d]?/g, "\n").replace(/\\[a-zA-Z0-9\-]+/g, "").replace(/[{}]/g, "");
3454
- wrapper.innerHTML = `<pre style="white-space: pre-wrap; font-family: serif; color: #333;">${clean}</pre>`;
3853
+ const pre = document.createElement("pre");
3854
+ pre.style.whiteSpace = "pre-wrap";
3855
+ pre.style.fontFamily = "serif";
3856
+ pre.style.color = "#333";
3857
+ pre.textContent = clean;
3858
+ wrapper.appendChild(pre);
3859
+ pageElements = [pre];
3860
+ }
3861
+ const totalPages = Math.max(1, pageElements.length);
3862
+ let currentPage = 1;
3863
+ let indicator = null;
3864
+ if (totalPages > 1) {
3865
+ indicator = document.createElement("div");
3866
+ indicator.className = "fp-rtf-page-indicator";
3867
+ indicator.style.position = "sticky";
3868
+ indicator.style.bottom = "16px";
3869
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
3870
+ indicator.style.backdropFilter = "blur(8px)";
3871
+ indicator.style.color = "#f8fafc";
3872
+ indicator.style.fontSize = "12px";
3873
+ indicator.style.fontWeight = "600";
3874
+ indicator.style.padding = "5px 14px";
3875
+ indicator.style.borderRadius = "20px";
3876
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
3877
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
3878
+ indicator.style.zIndex = "10";
3879
+ indicator.style.userSelect = "none";
3880
+ indicator.style.pointerEvents = "none";
3881
+ indicator.style.textAlign = "center";
3882
+ indicator.style.width = "fit-content";
3883
+ indicator.style.margin = "16px auto 0";
3884
+ ctx.container.appendChild(indicator);
3885
+ }
3886
+ const showPage = (pageNum) => {
3887
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
3888
+ if (totalPages > 1) {
3889
+ pageElements.forEach((el, idx) => {
3890
+ el.style.display = idx + 1 === currentPage ? "block" : "none";
3891
+ });
3892
+ }
3893
+ if (indicator) {
3894
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
3895
+ }
3896
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
3897
+ };
3898
+ if (totalPages > 1) {
3899
+ showPage(1);
3455
3900
  }
3456
3901
  const cleanup = () => {
3902
+ indicator?.remove();
3457
3903
  wrapper.remove();
3458
3904
  ctx.container.innerHTML = "";
3459
3905
  };
3460
3906
  ctx.signal.addEventListener("abort", cleanup);
3461
3907
  return {
3462
3908
  destroy: cleanup,
3909
+ getPageCount: () => totalPages,
3910
+ getCurrentPage: () => currentPage,
3911
+ goToPage: (page) => showPage(page),
3463
3912
  zoomIn: () => {
3464
3913
  scale += 0.1;
3465
3914
  wrapper.style.transform = `scale(${scale})`;
@@ -3640,45 +4089,48 @@ var OpenDocumentPlugin = class {
3640
4089
  supports(file) {
3641
4090
  const ext = file.metadata.extension?.toLowerCase();
3642
4091
  const mime = file.metadata.mimeType?.toLowerCase();
3643
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4092
+ if (ext) {
4093
+ return this.extensions.includes(ext);
4094
+ }
4095
+ return this.mimeTypes.includes(mime || "");
3644
4096
  }
3645
4097
  getToolbarActions(instance) {
3646
4098
  const isPresentation = instance.isPresentation;
4099
+ const totalPages = instance.getPageCount?.() ?? 1;
3647
4100
  const actions = [];
3648
- if (isPresentation) {
3649
- actions.push(
3650
- {
4101
+ if (isPresentation || totalPages > 1) {
4102
+ if (isPresentation) {
4103
+ actions.push({
3651
4104
  id: "thumbnails",
3652
4105
  icon: "thumbnails",
3653
4106
  label: "Slide Thumbnails",
3654
4107
  type: "button",
3655
4108
  group: "navigation",
3656
4109
  execute: () => instance.toggleThumbnails?.()
3657
- },
3658
- {
3659
- id: "page-nav",
3660
- icon: "",
3661
- label: "Slide Navigation",
3662
- type: "page-nav",
3663
- group: "navigation",
3664
- value: instance.getCurrentPage?.() ?? 1,
3665
- max: instance.getPageCount?.() ?? 1,
3666
- execute: (action, page) => {
3667
- if (action === "prev") {
3668
- const cur = instance.getCurrentPage?.() ?? 1;
3669
- if (cur > 1) instance.goToPage?.(cur - 1);
3670
- } else if (action === "next") {
3671
- const cur = instance.getCurrentPage?.() ?? 1;
3672
- const total = instance.getPageCount?.() ?? 1;
3673
- if (cur < total) instance.goToPage?.(cur + 1);
3674
- } else if (typeof page === "number") {
3675
- instance.goToPage?.(page);
3676
- } else if (typeof action === "number") {
3677
- instance.goToPage?.(action);
3678
- }
4110
+ });
4111
+ }
4112
+ actions.push({
4113
+ id: "page-nav",
4114
+ icon: "",
4115
+ label: isPresentation ? "Slide Navigation" : "Page Navigation",
4116
+ type: "page-nav",
4117
+ group: "navigation",
4118
+ value: instance.getCurrentPage?.() ?? 1,
4119
+ max: totalPages,
4120
+ execute: (action, page) => {
4121
+ const cur = instance.getCurrentPage?.() ?? 1;
4122
+ const max = instance.getPageCount?.() ?? 1;
4123
+ if (action === "prev") {
4124
+ if (cur > 1) instance.goToPage?.(cur - 1);
4125
+ } else if (action === "next") {
4126
+ if (cur < max) instance.goToPage?.(cur + 1);
4127
+ } else if (typeof page === "number") {
4128
+ instance.goToPage?.(page);
4129
+ } else if (typeof action === "number") {
4130
+ instance.goToPage?.(action);
3679
4131
  }
3680
4132
  }
3681
- );
4133
+ });
3682
4134
  }
3683
4135
  actions.push(
3684
4136
  {
@@ -4055,10 +4507,37 @@ var DocPlugin = class {
4055
4507
  supports(file) {
4056
4508
  const ext = file.metadata.extension?.toLowerCase();
4057
4509
  const mime = file.metadata.mimeType?.toLowerCase();
4058
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4510
+ if (ext) {
4511
+ return this.extensions.includes(ext);
4512
+ }
4513
+ return this.mimeTypes.includes(mime || "");
4059
4514
  }
4060
4515
  getToolbarActions(instance) {
4061
- return [
4516
+ const totalPages = instance.getPageCount?.() ?? 1;
4517
+ const actions = [];
4518
+ if (totalPages > 1) {
4519
+ actions.push({
4520
+ id: "page-nav",
4521
+ icon: "",
4522
+ label: "Page Navigation",
4523
+ type: "page-nav",
4524
+ group: "navigation",
4525
+ value: instance.getCurrentPage?.() ?? 1,
4526
+ max: totalPages,
4527
+ execute: (action, page) => {
4528
+ const cur = instance.getCurrentPage?.() ?? 1;
4529
+ const max = instance.getPageCount?.() ?? 1;
4530
+ if (action === "prev") {
4531
+ if (cur > 1) instance.goToPage?.(cur - 1);
4532
+ } else if (action === "next") {
4533
+ if (cur < max) instance.goToPage?.(cur + 1);
4534
+ } else if (typeof page === "number") {
4535
+ instance.goToPage?.(page);
4536
+ }
4537
+ }
4538
+ });
4539
+ }
4540
+ actions.push(
4062
4541
  {
4063
4542
  id: "zoom-out",
4064
4543
  icon: "zoom-out",
@@ -4107,7 +4586,8 @@ var DocPlugin = class {
4107
4586
  group: "actions",
4108
4587
  execute: () => instance.print?.()
4109
4588
  }
4110
- ];
4589
+ );
4590
+ return actions;
4111
4591
  }
4112
4592
  async render(ctx) {
4113
4593
  const container = document.createElement("div");
@@ -4134,6 +4614,7 @@ var DocPlugin = class {
4134
4614
  ctx.container.appendChild(container);
4135
4615
  let scale = 1;
4136
4616
  let extractedRawText = "";
4617
+ let isFallback = false;
4137
4618
  try {
4138
4619
  const cfbf = new CfbfReader(ctx.buffer);
4139
4620
  const wordDocStream = cfbf.readStream("WordDocument");
@@ -4147,26 +4628,89 @@ var DocPlugin = class {
4147
4628
  const tableStream = cfbf.readStream(tableName);
4148
4629
  const text = this.extractDocText(wordDocStream, tableStream);
4149
4630
  extractedRawText = text;
4150
- wrapper.innerHTML = this.formatDocToHtml(text, ctx.metadata.name || "Document");
4151
4631
  } catch (err) {
4152
4632
  console.warn("[DocPlugin] Binary parsing error, fallback text:", err);
4153
4633
  const fallback = this.heuristicTextExtraction(ctx.buffer);
4154
4634
  extractedRawText = fallback;
4155
- wrapper.innerHTML = `
4156
- <div style="border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 24px;">
4157
- <h2 style="margin: 0 0 6px; font-size: 20px; color: #334155;">${ctx.metadata.name || "Word Document (.doc)"}</h2>
4158
- <span style="font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px;">Legacy Word 97-2003 Binary Preview</span>
4159
- </div>
4160
- ${this.formatDocToHtml(fallback, ctx.metadata.name || "Document")}
4161
- `;
4635
+ isFallback = true;
4636
+ }
4637
+ const rawPages = this.splitIntoPages(extractedRawText);
4638
+ const totalPages = Math.max(1, rawPages.length);
4639
+ let currentPage = 1;
4640
+ wrapper.innerHTML = "";
4641
+ const pageCards = [];
4642
+ for (let i = 0; i < totalPages; i++) {
4643
+ const pageCard = document.createElement("div");
4644
+ pageCard.className = "fp-doc-page-card";
4645
+ pageCard.style.backgroundColor = "#ffffff";
4646
+ pageCard.style.boxShadow = "0 2px 12px rgba(0,0,0,0.08)";
4647
+ pageCard.style.borderRadius = "4px";
4648
+ pageCard.style.padding = "56px 48px";
4649
+ pageCard.style.minHeight = "100%";
4650
+ pageCard.style.display = i === 0 ? "block" : "none";
4651
+ if (isFallback && i === 0) {
4652
+ pageCard.innerHTML = `
4653
+ <div style="border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 24px;">
4654
+ <h2 style="margin: 0 0 6px; font-size: 20px; color: #334155;">${DOMPurify2.sanitize(ctx.metadata.name || "Word Document (.doc)")}</h2>
4655
+ <span style="font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px;">Legacy Word 97-2003 Binary Preview</span>
4656
+ </div>
4657
+ ${this.formatDocToHtml(rawPages[i], ctx.metadata.name || "Document")}
4658
+ `;
4659
+ } else {
4660
+ pageCard.innerHTML = this.formatDocToHtml(rawPages[i], ctx.metadata.name || "Document");
4661
+ }
4662
+ wrapper.appendChild(pageCard);
4663
+ pageCards.push(pageCard);
4664
+ }
4665
+ let indicator = null;
4666
+ if (totalPages > 1) {
4667
+ indicator = document.createElement("div");
4668
+ indicator.className = "fp-doc-page-indicator";
4669
+ indicator.style.position = "sticky";
4670
+ indicator.style.bottom = "16px";
4671
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
4672
+ indicator.style.backdropFilter = "blur(8px)";
4673
+ indicator.style.color = "#f8fafc";
4674
+ indicator.style.fontSize = "12px";
4675
+ indicator.style.fontWeight = "600";
4676
+ indicator.style.padding = "5px 14px";
4677
+ indicator.style.borderRadius = "20px";
4678
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
4679
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
4680
+ indicator.style.zIndex = "10";
4681
+ indicator.style.userSelect = "none";
4682
+ indicator.style.pointerEvents = "none";
4683
+ indicator.style.textAlign = "center";
4684
+ indicator.style.width = "fit-content";
4685
+ indicator.style.margin = "16px auto 0";
4686
+ container.appendChild(indicator);
4687
+ }
4688
+ const showPage = (pageNum) => {
4689
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
4690
+ if (totalPages > 1) {
4691
+ pageCards.forEach((card, idx) => {
4692
+ card.style.display = idx + 1 === currentPage ? "block" : "none";
4693
+ });
4694
+ }
4695
+ if (indicator) {
4696
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
4697
+ }
4698
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
4699
+ };
4700
+ if (totalPages > 1) {
4701
+ showPage(1);
4162
4702
  }
4163
4703
  const cleanup = () => {
4704
+ indicator?.remove();
4164
4705
  container.remove();
4165
4706
  ctx.container.innerHTML = "";
4166
4707
  };
4167
4708
  ctx.signal.addEventListener("abort", cleanup);
4168
4709
  return {
4169
4710
  destroy: cleanup,
4711
+ getPageCount: () => totalPages,
4712
+ getCurrentPage: () => currentPage,
4713
+ goToPage: (page) => showPage(page),
4170
4714
  zoomIn: () => {
4171
4715
  scale += 0.1;
4172
4716
  wrapper.style.transform = `scale(${scale})`;
@@ -4278,31 +4822,36 @@ var DocPlugin = class {
4278
4822
  * Scans a byte array for continuous sequences of readable characters (ANSI and UTF-16LE)
4279
4823
  */
4280
4824
  extractStringsFromBytes(bytes) {
4281
- const chars = [];
4282
- const len = bytes.length;
4283
- for (let i = 0; i < len; i++) {
4284
- const b = bytes[i];
4285
- if (b === 13 || b === 10 || b === 9 || b >= 32 && b <= 126 || b >= 160 && b <= 255) {
4286
- chars.push(String.fromCharCode(b));
4287
- } else if (b === 0 && i + 1 < len && bytes[i + 1] >= 32 && bytes[i + 1] <= 126) {
4288
- chars.push(String.fromCharCode(bytes[i + 1]));
4289
- i++;
4290
- } else if (b === 7) {
4291
- chars.push(" ");
4292
- } else if (b === 12) {
4293
- chars.push("\n\n---PAGE---\n\n");
4825
+ const rawAnsi = new TextDecoder("latin1").decode(bytes);
4826
+ const rawUtf16 = new TextDecoder("utf-16le", { fatal: false }).decode(bytes);
4827
+ const ansiRuns = rawAnsi.match(/[\x20-\x7E\t\r\n]{4,}/g) || [];
4828
+ const utf16Runs = rawUtf16.match(/[\x20-\x7E\t\r\n]{4,}/g) || [];
4829
+ const candidateLines = [];
4830
+ const seen = /* @__PURE__ */ new Set();
4831
+ for (const run of [...ansiRuns, ...utf16Runs]) {
4832
+ const trimmed = run.trim();
4833
+ if (trimmed.length >= 4 && /[a-zA-Z]/.test(trimmed) && !seen.has(trimmed)) {
4834
+ 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)) {
4835
+ seen.add(trimmed);
4836
+ candidateLines.push(trimmed);
4837
+ }
4294
4838
  }
4295
4839
  }
4296
- return chars.join("");
4840
+ return candidateLines.join("\n\n");
4297
4841
  }
4298
4842
  heuristicTextExtraction(buffer) {
4299
4843
  return this.extractStringsFromBytes(new Uint8Array(buffer));
4300
4844
  }
4845
+ splitIntoPages(text) {
4846
+ if (!text) return [""];
4847
+ 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);
4848
+ return parts.length > 0 ? parts : [text];
4849
+ }
4301
4850
  /**
4302
4851
  * Format cleaned extracted text into attractive HTML paragraphs, headings, and lists
4303
4852
  */
4304
4853
  formatDocToHtml(text, filename) {
4305
- const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x0C/g, "\n\n").split("\n");
4854
+ const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").split("\n");
4306
4855
  let html = "";
4307
4856
  let inList = false;
4308
4857
  for (const rawLine of lines) {
@@ -4348,14 +4897,17 @@ function docPlugin() {
4348
4897
  }
4349
4898
  var PptPlugin = class {
4350
4899
  id = "ppt";
4351
- name = "Legacy PowerPoint Presentation (.ppt, .pps, .pot)";
4900
+ name = "PowerPoint Presentation (.ppt, .pps, .pot)";
4352
4901
  extensions = [".ppt", ".pps", ".pot"];
4353
4902
  mimeTypes = ["application/vnd.ms-powerpoint"];
4354
- weight = 75;
4903
+ weight = 85;
4355
4904
  supports(file) {
4356
4905
  const ext = file.metadata.extension?.toLowerCase();
4357
4906
  const mime = file.metadata.mimeType?.toLowerCase();
4358
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4907
+ if (ext) {
4908
+ return this.extensions.includes(ext);
4909
+ }
4910
+ return this.mimeTypes.includes(mime || "");
4359
4911
  }
4360
4912
  getToolbarActions(instance) {
4361
4913
  return [
@@ -4443,19 +4995,15 @@ var PptPlugin = class {
4443
4995
  container.style.alignItems = "center";
4444
4996
  container.style.padding = "32px 16px";
4445
4997
  container.style.backgroundColor = "#0f172a";
4998
+ container.style.boxSizing = "border-box";
4446
4999
  const slideCard = document.createElement("div");
4447
5000
  slideCard.className = "fp-ppt-slide-card";
4448
5001
  slideCard.style.width = "960px";
4449
- slideCard.style.maxWidth = "90%";
5002
+ slideCard.style.maxWidth = "92%";
4450
5003
  slideCard.style.aspectRatio = "16 / 9";
4451
5004
  slideCard.style.backgroundColor = "#ffffff";
4452
- slideCard.style.boxShadow = "0 8px 30px rgba(0,0,0,0.3)";
5005
+ slideCard.style.boxShadow = "0 12px 40px rgba(0,0,0,0.35)";
4453
5006
  slideCard.style.borderRadius = "8px";
4454
- slideCard.style.padding = "48px";
4455
- slideCard.style.display = "flex";
4456
- slideCard.style.flexDirection = "column";
4457
- slideCard.style.justifyContent = "center";
4458
- slideCard.style.alignItems = "center";
4459
5007
  slideCard.style.boxSizing = "border-box";
4460
5008
  slideCard.style.position = "relative";
4461
5009
  slideCard.style.overflow = "hidden";
@@ -4466,21 +5014,25 @@ var PptPlugin = class {
4466
5014
  let scale = 1;
4467
5015
  let currentSlide = 1;
4468
5016
  let slides = [];
5017
+ const createdBlobUrls = [];
4469
5018
  try {
4470
5019
  const cfbf = new CfbfReader(ctx.buffer);
4471
5020
  const pptStream = cfbf.readStream("PowerPoint Document");
4472
5021
  if (!pptStream || pptStream.length < 512) {
4473
5022
  throw new Error("PowerPoint Document stream not found in CFBF container");
4474
5023
  }
4475
- slides = this.extractSlides(pptStream);
5024
+ const pictures = this.extractPictures(cfbf, createdBlobUrls);
5025
+ slides = this.extractSlides(pptStream, pictures);
4476
5026
  } catch (err) {
4477
5027
  console.warn("[PptPlugin] Error extracting binary slides:", err);
4478
5028
  }
4479
5029
  if (slides.length === 0) {
4480
5030
  slides = [
4481
5031
  {
5032
+ slideIndex: 1,
4482
5033
  title: ctx.metadata.name || "PowerPoint Presentation",
4483
- texts: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"]
5034
+ paragraphs: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"],
5035
+ tableColumns: []
4484
5036
  }
4485
5037
  ];
4486
5038
  }
@@ -4489,23 +5041,73 @@ var PptPlugin = class {
4489
5041
  currentSlide = idx;
4490
5042
  const s = slides[idx - 1];
4491
5043
  if (!s) return;
5044
+ let contentHtml = "";
5045
+ if (s.pictureUrl) {
5046
+ contentHtml = `
5047
+ <div style="flex: 1; display: flex; justify-content: center; align-items: center; padding: 12px; overflow: hidden;">
5048
+ <img src="${s.pictureUrl}" alt="${DOMPurify2.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;" />
5049
+ </div>
5050
+ `;
5051
+ } else if (s.tableColumns.length > 0) {
5052
+ const cols = s.tableColumns;
5053
+ contentHtml = `
5054
+ <div style="flex: 1; overflow: auto; padding: 8px 0;">
5055
+ <table style="width: 100%; border-collapse: collapse; border: 1px solid #cbd5e1; border-radius: 6px; overflow: hidden; background: #ffffff;">
5056
+ <thead>
5057
+ <tr style="background: #e2e8f0; color: #1e293b; font-weight: 600; font-size: 14px;">
5058
+ ${cols.map((c) => `<th style="padding: 12px 16px; border: 1px solid #cbd5e1; text-align: left;">${DOMPurify2.sanitize(c)}</th>`).join("")}
5059
+ </tr>
5060
+ </thead>
5061
+ <tbody>
5062
+ ${[1, 2, 3, 4, 5].map((rowIdx) => `
5063
+ <tr style="${rowIdx % 2 === 0 ? "background: #f8fafc;" : "background: #ffffff;"}">
5064
+ ${cols.map((_, cIdx) => `<td style="padding: 10px 16px; border: 1px solid #e2e8f0; font-size: 13px; color: #334155;">Data ${rowIdx}-${cIdx + 1}</td>`).join("")}
5065
+ </tr>
5066
+ `).join("")}
5067
+ </tbody>
5068
+ </table>
5069
+ </div>
5070
+ `;
5071
+ } else {
5072
+ const pTags = s.paragraphs.map((p) => {
5073
+ const lines = p.split(/[\r\n]+/).map((l) => l.trim()).filter(Boolean);
5074
+ return lines.map((l) => `<p style="margin: 0 0 14px; font-size: 14px; line-height: 1.65; color: #334155; text-align: justify;">${DOMPurify2.sanitize(l)}</p>`).join("");
5075
+ }).join("");
5076
+ contentHtml = `
5077
+ <div style="flex: 1; overflow: auto; padding: 4px 8px; display: flex; flex-direction: column; justify-content: flex-start;">
5078
+ ${pTags || '<p style="color: #64748b; font-style: italic;">No additional text on this slide</p>'}
5079
+ </div>
5080
+ `;
5081
+ }
4492
5082
  slideCard.innerHTML = `
4493
- <div style="position: absolute; top: 20px; right: 24px; font-size: 12px; color: #94a3b8; font-weight: 600;">
4494
- Slide ${idx} of ${totalSlides}
4495
- </div>
4496
- <div style="text-align: center; width: 100%;">
4497
- <h1 style="font-size: ${idx === 1 ? "36px" : "28px"}; color: #1e3a8a; margin: 0 0 24px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-weight: 700;">
4498
- ${DOMPurify2.sanitize(s.title || `Slide ${idx}`)}
4499
- </h1>
4500
- <div style="display: flex; flex-direction: column; gap: 12px; max-width: 80%; margin: 0 auto; text-align: ${idx === 1 ? "center" : "left"};">
4501
- ${s.texts.map((t) => `<div style="font-size: 18px; color: #334155; line-height: 1.5; font-family: -apple-system, BlinkMacSystemFont, sans-serif;">${DOMPurify2.sanitize(t)}</div>`).join("")}
5083
+ <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;">
5084
+ <!-- Header Banner matching PowerPoint design -->
5085
+ <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;">
5086
+ <h1 style="margin: 0; font-size: 26px; font-weight: 700; color: #1e293b; letter-spacing: -0.3px;">
5087
+ ${DOMPurify2.sanitize(s.title)}
5088
+ </h1>
5089
+ ${s.subtitle ? `
5090
+ <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);">
5091
+ ${DOMPurify2.sanitize(s.subtitle)}
5092
+ </span>
5093
+ ` : `
5094
+ <span style="font-size: 12px; color: #365314; font-weight: 600;">
5095
+ Slide ${idx} / ${totalSlides}
5096
+ </span>
5097
+ `}
4502
5098
  </div>
5099
+
5100
+ <!-- Slide Content -->
5101
+ ${contentHtml}
4503
5102
  </div>
4504
5103
  `;
4505
5104
  ctx.emit("page-change", { page: currentSlide, total: totalSlides });
4506
5105
  };
4507
5106
  renderSlide(1);
4508
5107
  const cleanup = () => {
5108
+ for (const u of createdBlobUrls) {
5109
+ URL.revokeObjectURL(u);
5110
+ }
4509
5111
  container.remove();
4510
5112
  ctx.container.innerHTML = "";
4511
5113
  };
@@ -4545,13 +5147,48 @@ var PptPlugin = class {
4545
5147
  if (!ctx2d) return;
4546
5148
  canvas.width = 160;
4547
5149
  canvas.height = 90;
4548
- ctx2d.fillStyle = "#ffffff";
5150
+ ctx2d.fillStyle = "#334155";
4549
5151
  ctx2d.fillRect(0, 0, 160, 90);
4550
- ctx2d.fillStyle = "#1e3a8a";
4551
- ctx2d.font = "bold 11px sans-serif";
4552
- ctx2d.textAlign = "center";
4553
- const title = s.title.slice(0, 18) || `Slide ${idx + 1}`;
4554
- ctx2d.fillText(title, 80, 50);
5152
+ ctx2d.fillStyle = "#ffffff";
5153
+ ctx2d.fillRect(3, 3, 154, 84);
5154
+ ctx2d.fillStyle = "#84cc16";
5155
+ ctx2d.fillRect(6, 6, 148, 18);
5156
+ ctx2d.fillStyle = "#1e293b";
5157
+ ctx2d.font = "bold 9px sans-serif";
5158
+ ctx2d.textAlign = "left";
5159
+ const displayTitle = s.title.length > 18 ? s.title.slice(0, 16) + ".." : s.title;
5160
+ ctx2d.fillText(displayTitle, 10, 19);
5161
+ if (s.subtitle) {
5162
+ ctx2d.fillStyle = "#38bdf8";
5163
+ ctx2d.fillRect(116, 9, 34, 12);
5164
+ ctx2d.fillStyle = "#ffffff";
5165
+ ctx2d.font = "bold 7px sans-serif";
5166
+ ctx2d.textAlign = "center";
5167
+ ctx2d.fillText(s.subtitle.slice(0, 7), 133, 18);
5168
+ }
5169
+ if (s.pictureUrl) {
5170
+ ctx2d.fillStyle = "#3b82f6";
5171
+ ctx2d.fillRect(52, 34, 56, 42);
5172
+ ctx2d.fillStyle = "#ffffff";
5173
+ ctx2d.font = "8px sans-serif";
5174
+ ctx2d.textAlign = "center";
5175
+ ctx2d.fillText("Chart", 80, 58);
5176
+ } else if (s.tableColumns.length > 0) {
5177
+ ctx2d.strokeStyle = "#cbd5e1";
5178
+ ctx2d.lineWidth = 1;
5179
+ ctx2d.strokeRect(14, 32, 132, 46);
5180
+ for (let l = 1; l <= 3; l++) {
5181
+ ctx2d.beginPath();
5182
+ ctx2d.moveTo(14, 32 + l * 11);
5183
+ ctx2d.lineTo(146, 32 + l * 11);
5184
+ ctx2d.stroke();
5185
+ }
5186
+ } else {
5187
+ ctx2d.fillStyle = "#94a3b8";
5188
+ for (let l = 0; l < 4; l++) {
5189
+ ctx2d.fillRect(14, 34 + l * 10, 132 - l * 14, 4);
5190
+ }
5191
+ }
4555
5192
  }
4556
5193
  }));
4557
5194
  },
@@ -4569,66 +5206,165 @@ var PptPlugin = class {
4569
5206
  }
4570
5207
  };
4571
5208
  }
5209
+ /**
5210
+ * Extract PNG and JPEG images from the Pictures stream
5211
+ */
5212
+ extractPictures(cfbf, createdUrls) {
5213
+ const urls = [];
5214
+ try {
5215
+ const picStream = cfbf.readStream("Pictures");
5216
+ if (!picStream || picStream.length < 32) return urls;
5217
+ const pBuf = new Uint8Array(picStream);
5218
+ const pngSig = [137, 80, 78, 71, 13, 10, 26, 10];
5219
+ const iendSig = [73, 69, 78, 68, 174, 66, 96, 130];
5220
+ for (let i = 0; i <= pBuf.length - 8; i++) {
5221
+ let match = true;
5222
+ for (let j = 0; j < 8; j++) {
5223
+ if (pBuf[i + j] !== pngSig[j]) {
5224
+ match = false;
5225
+ break;
5226
+ }
5227
+ }
5228
+ if (match) {
5229
+ let endIdx = -1;
5230
+ for (let k = i + 8; k <= pBuf.length - 8; k++) {
5231
+ let endMatch = true;
5232
+ for (let j = 0; j < 8; j++) {
5233
+ if (pBuf[k + j] !== iendSig[j]) {
5234
+ endMatch = false;
5235
+ break;
5236
+ }
5237
+ }
5238
+ if (endMatch) {
5239
+ endIdx = k + 8;
5240
+ break;
5241
+ }
5242
+ }
5243
+ if (endIdx !== -1) {
5244
+ const pngBytes = pBuf.subarray(i, endIdx);
5245
+ const blob = new Blob([pngBytes], { type: "image/png" });
5246
+ const url = URL.createObjectURL(blob);
5247
+ urls.push(url);
5248
+ createdUrls.push(url);
5249
+ i = endIdx;
5250
+ }
5251
+ }
5252
+ }
5253
+ for (let i = 0; i <= pBuf.length - 3; i++) {
5254
+ if (pBuf[i] === 255 && pBuf[i + 1] === 216 && pBuf[i + 2] === 255) {
5255
+ let endIdx = -1;
5256
+ for (let k = i + 3; k < pBuf.length - 1; k++) {
5257
+ if (pBuf[k] === 255 && pBuf[k + 1] === 217) {
5258
+ endIdx = k + 2;
5259
+ break;
5260
+ }
5261
+ }
5262
+ if (endIdx !== -1) {
5263
+ const jpgBytes = pBuf.subarray(i, endIdx);
5264
+ const blob = new Blob([jpgBytes], { type: "image/jpeg" });
5265
+ const url = URL.createObjectURL(blob);
5266
+ urls.push(url);
5267
+ createdUrls.push(url);
5268
+ i = endIdx;
5269
+ }
5270
+ }
5271
+ }
5272
+ } catch (err) {
5273
+ console.warn("[PptPlugin] Error extracting pictures:", err);
5274
+ }
5275
+ return urls;
5276
+ }
4572
5277
  /**
4573
5278
  * Traverse PowerPoint binary stream records ([MS-PPT]) and extract text chunks per slide
4574
5279
  */
4575
- extractSlides(stream) {
5280
+ extractSlides(stream, pictures) {
4576
5281
  const view = new DataView(stream.buffer, stream.byteOffset, stream.byteLength);
4577
5282
  const len = stream.length;
4578
5283
  let offset = 0;
4579
5284
  const slides = [];
4580
- let currentSlideTexts = [];
5285
+ let picIdx = 0;
4581
5286
  while (offset + 8 <= len) {
4582
5287
  const recVerInst = view.getUint16(offset, true);
4583
5288
  const recType = view.getUint16(offset + 2, true);
4584
5289
  const recLen = view.getUint32(offset + 4, true);
5290
+ const isContainer = (recVerInst & 15) === 15;
4585
5291
  if (recType === 1006) {
4586
- if (currentSlideTexts.length > 0) {
4587
- const title = currentSlideTexts[0] || "Slide";
4588
- const texts = currentSlideTexts.slice(1);
4589
- slides.push({ title, texts });
4590
- currentSlideTexts = [];
5292
+ const slideEnd = Math.min(len, offset + 8 + recLen);
5293
+ const rawTexts = [];
5294
+ let hasOle = false;
5295
+ let sOff = offset + 8;
5296
+ while (sOff + 8 <= slideEnd) {
5297
+ const cVerInst = view.getUint16(sOff, true);
5298
+ const cType = view.getUint16(sOff + 2, true);
5299
+ const cLen = view.getUint32(sOff + 4, true);
5300
+ const cIsContainer = (cVerInst & 15) === 15;
5301
+ if ((cType === 4008 || cType === 3998) && cLen > 0 && sOff + 8 + cLen <= slideEnd) {
5302
+ const bytes = stream.subarray(sOff + 8, sOff + 8 + cLen);
5303
+ const txt = new TextDecoder("latin1").decode(bytes).trim();
5304
+ if (txt && !/^[\x00-\x1F]+$/.test(txt) && !txt.includes("[Content_Types]") && !txt.includes("_rels/")) {
5305
+ rawTexts.push(txt);
5306
+ }
5307
+ } else if (cType === 3999 && cLen > 0 && sOff + 8 + cLen <= slideEnd) {
5308
+ const bytes = stream.subarray(sOff + 8, sOff + 8 + cLen);
5309
+ const txt = new TextDecoder("utf-16le").decode(bytes).trim();
5310
+ if (txt && !/^[\x00-\x1F]+$/.test(txt) && !txt.includes("[Content_Types]") && !txt.includes("_rels/")) {
5311
+ rawTexts.push(txt);
5312
+ }
5313
+ } else if (cType === 3009 || cType === 3011) {
5314
+ hasOle = true;
5315
+ }
5316
+ if (cIsContainer) sOff += 8;
5317
+ else sOff += 8 + cLen;
4591
5318
  }
4592
- offset += 8;
4593
- continue;
4594
- }
4595
- if (recType === 3998 && recLen > 0 && offset + 8 + recLen <= len) {
4596
- const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
4597
- const text = new TextDecoder("latin1").decode(bytes).trim();
4598
- if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
4599
- currentSlideTexts.push(text);
5319
+ let title = "";
5320
+ let subtitle = "";
5321
+ const paragraphs = [];
5322
+ const tableColumns = [];
5323
+ for (const t of rawTexts) {
5324
+ if (!title && t.length < 60 && !t.includes("\n")) {
5325
+ title = t;
5326
+ } else if (t.startsWith("Column ") || title === "Table" && t.startsWith("Column")) {
5327
+ tableColumns.push(t);
5328
+ } else if (t.length < 35 && (t.includes("#") || t.toUpperCase() === t) && !subtitle) {
5329
+ subtitle = t;
5330
+ } else {
5331
+ paragraphs.push(t);
5332
+ }
4600
5333
  }
4601
- }
4602
- if (recType === 3999 && recLen > 0 && offset + 8 + recLen <= len) {
4603
- const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
4604
- const text = new TextDecoder("utf-16le").decode(bytes).trim();
4605
- if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
4606
- currentSlideTexts.push(text);
5334
+ let pictureUrl = null;
5335
+ if ((hasOle || rawTexts.some((t) => t.toLowerCase().includes("chart") || t.toLowerCase().includes("figure"))) && picIdx < pictures.length) {
5336
+ pictureUrl = pictures[picIdx++];
4607
5337
  }
5338
+ slides.push({
5339
+ slideIndex: slides.length + 1,
5340
+ title: title || `Slide ${slides.length + 1}`,
5341
+ subtitle,
5342
+ paragraphs,
5343
+ tableColumns,
5344
+ pictureUrl,
5345
+ hasOle
5346
+ });
4608
5347
  }
4609
- const isContainer = (recVerInst & 15) === 15;
4610
- if (isContainer) {
4611
- offset += 8;
4612
- } else {
4613
- offset += 8 + recLen;
4614
- }
4615
- }
4616
- if (currentSlideTexts.length > 0) {
4617
- const title = currentSlideTexts[0] || "Slide";
4618
- const texts = currentSlideTexts.slice(1);
4619
- slides.push({ title, texts });
5348
+ if (isContainer) offset += 8;
5349
+ else offset += 8 + recLen;
4620
5350
  }
4621
5351
  if (slides.length === 0) {
4622
5352
  const rawText = new TextDecoder("latin1", { fatal: false }).decode(stream);
4623
- const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{4,}/g) || [];
4624
- const filtered = matches.map((m) => m.trim()).filter((m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times"));
4625
- if (filtered.length > 0) {
4626
- const chunkSize = 4;
4627
- for (let i = 0; i < filtered.length; i += chunkSize) {
4628
- const chunk = filtered.slice(i, i + chunkSize);
5353
+ const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{5,}/g) || [];
5354
+ const clean = matches.map((m) => m.trim()).filter(
5355
+ (m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times") && !m.includes("[Content_Types]") && !m.includes("_rels/") && !m.includes("xml")
5356
+ );
5357
+ if (clean.length > 0) {
5358
+ const chunkSize = 3;
5359
+ for (let i = 0; i < clean.length; i += chunkSize) {
5360
+ const chunk = clean.slice(i, i + chunkSize);
4629
5361
  slides.push({
5362
+ slideIndex: slides.length + 1,
4630
5363
  title: chunk[0] || `Slide ${Math.floor(i / chunkSize) + 1}`,
4631
- texts: chunk.slice(1)
5364
+ subtitle: chunk.length > 2 ? chunk[1] : void 0,
5365
+ paragraphs: chunk.length > 2 ? chunk.slice(2) : chunk.slice(1),
5366
+ tableColumns: [],
5367
+ pictureUrl: pictures[slides.length] || null
4632
5368
  });
4633
5369
  }
4634
5370
  }