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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import DOMPurify2 from 'dompurify';
2
+ import * as pdfjsLib from 'pdfjs-dist';
2
3
  import * as docx from 'docx-preview';
3
4
  import { unzipSync, strFromU8, unzip } from 'fflate';
4
5
  import * as XLSX from 'xlsx';
@@ -9,7 +10,7 @@ import * as THREE from 'three';
9
10
  import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
10
11
  import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
11
12
  import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
12
- import { RTFJS } from 'rtf.js';
13
+ import * as RTFJS from 'rtf.js/dist/RTFJS.bundle.js';
13
14
 
14
15
  // ../core/dist/index.js
15
16
  var EventEmitter = class {
@@ -265,6 +266,39 @@ function detectOoxmlType(buffer) {
265
266
  }
266
267
  return "application/zip";
267
268
  }
269
+ function detectCfbfType(buffer) {
270
+ const bytes = new Uint8Array(buffer);
271
+ const hasUtf16le = (str) => {
272
+ const target = new Uint8Array(str.length * 2);
273
+ for (let i = 0; i < str.length; i++) {
274
+ target[i * 2] = str.charCodeAt(i);
275
+ target[i * 2 + 1] = 0;
276
+ }
277
+ const targetLen = target.length;
278
+ const max = bytes.length - targetLen;
279
+ for (let i = 0; i <= max; i++) {
280
+ let match = true;
281
+ for (let j = 0; j < targetLen; j++) {
282
+ if (bytes[i + j] !== target[j]) {
283
+ match = false;
284
+ break;
285
+ }
286
+ }
287
+ if (match) return true;
288
+ }
289
+ return false;
290
+ };
291
+ if (hasUtf16le("PowerPoint Document")) {
292
+ return { mime: "application/vnd.ms-powerpoint", extension: ".ppt" };
293
+ }
294
+ if (hasUtf16le("WordDocument")) {
295
+ return { mime: "application/msword", extension: ".doc" };
296
+ }
297
+ if (hasUtf16le("Workbook") || hasUtf16le("Book")) {
298
+ return { mime: "application/vnd.ms-excel", extension: ".xls" };
299
+ }
300
+ return null;
301
+ }
268
302
  function extractExtension(nameOrUrl) {
269
303
  try {
270
304
  const url = new URL(nameOrUrl);
@@ -329,6 +363,18 @@ async function sourceToArrayBuffer(source, signal) {
329
363
  } else {
330
364
  metadata.mimeType = metadata.mimeType ?? magicMime;
331
365
  }
366
+ } else if (magicMime === "application/x-cfbf") {
367
+ if (metadata.extension) {
368
+ metadata.mimeType = mimeFromExtension(metadata.extension) ?? magicMime;
369
+ } else {
370
+ const cfbf = detectCfbfType(buffer);
371
+ if (cfbf) {
372
+ metadata.mimeType = cfbf.mime;
373
+ metadata.extension = cfbf.extension;
374
+ } else {
375
+ metadata.mimeType = magicMime;
376
+ }
377
+ }
332
378
  } else {
333
379
  metadata.mimeType = magicMime;
334
380
  }
@@ -477,11 +523,21 @@ var ToolbarController = class {
477
523
  el;
478
524
  toolbarEl;
479
525
  actions = [];
526
+ pageInputEl = null;
527
+ pageLabelEl = null;
480
528
  constructor(container) {
481
529
  this.el = container;
482
530
  this.toolbarEl = createElement("div", { className: "fp-toolbar" });
483
531
  this.el.appendChild(this.toolbarEl);
484
532
  }
533
+ setPage(page, max) {
534
+ if (this.pageInputEl) {
535
+ this.pageInputEl.value = page.toString();
536
+ }
537
+ if (max !== void 0 && this.pageLabelEl) {
538
+ this.pageLabelEl.textContent = ` / ${max}`;
539
+ }
540
+ }
485
541
  update(actions) {
486
542
  this.actions = actions;
487
543
  this.render();
@@ -524,6 +580,7 @@ var ToolbarController = class {
524
580
  if (action.type === "separator") {
525
581
  groupEl.appendChild(createElement("div", { className: "fp-toolbar-separator" }));
526
582
  } else if (action.type === "page-nav") {
583
+ const max = action.max ?? 1;
527
584
  const prevBtn = this.createButton(
528
585
  "prev",
529
586
  ICON_PAGE_PREV,
@@ -542,7 +599,6 @@ var ToolbarController = class {
542
599
  "Next Page",
543
600
  () => {
544
601
  const cur = parseInt(input.value, 10) || 1;
545
- const max = action.max ?? 1;
546
602
  if (cur < max) {
547
603
  input.value = (cur + 1).toString();
548
604
  action.execute("next", cur + 1);
@@ -554,15 +610,18 @@ var ToolbarController = class {
554
610
  type: "number",
555
611
  value: (action.value ?? 1).toString(),
556
612
  min: "1",
557
- max: (action.max ?? 1).toString()
613
+ max: max.toString()
558
614
  });
559
615
  input.addEventListener("change", () => {
560
- const val = parseInt(input.value, 10);
561
- if (!isNaN(val)) {
562
- action.execute("go", val);
563
- }
616
+ let val = parseInt(input.value, 10);
617
+ if (isNaN(val)) val = 1;
618
+ val = Math.max(1, Math.min(max, val));
619
+ input.value = val.toString();
620
+ action.execute("go", val);
564
621
  });
565
- const label = createElement("span", { className: "fp-toolbar-label" }, ` / ${action.max ?? 1}`);
622
+ const label = createElement("span", { className: "fp-toolbar-label" }, ` / ${max}`);
623
+ this.pageInputEl = input;
624
+ this.pageLabelEl = label;
566
625
  groupEl.appendChild(prevBtn);
567
626
  groupEl.appendChild(input);
568
627
  groupEl.appendChild(label);
@@ -740,7 +799,13 @@ var FilePreviewViewer = class {
740
799
  buffer,
741
800
  options,
742
801
  signal,
743
- emit: (event, payload) => this.eventEmitter.emit(event, payload)
802
+ emit: (event, payload) => {
803
+ if (event === "page-change" && payload && typeof payload.page === "number") {
804
+ const total = payload.total ?? payload.totalPages;
805
+ this.toolbar?.setPage(payload.page, total);
806
+ }
807
+ this.eventEmitter.emit(event, payload);
808
+ }
744
809
  });
745
810
  this.activeInstance = instance;
746
811
  this.hideLoading();
@@ -774,6 +839,7 @@ var FilePreviewViewer = class {
774
839
  if (thumbnails && thumbnails.length > 0 && this.thumbnailPanel) {
775
840
  this.thumbnailPanel.update(thumbnails, (index) => {
776
841
  instance.goToPage?.(index + 1);
842
+ this.toolbar?.setPage(index + 1);
777
843
  });
778
844
  if (options.showThumbnails) {
779
845
  this.thumbnailPanel.show();
@@ -899,9 +965,13 @@ var FilePreviewViewer = class {
899
965
  if (e.key === "ArrowRight" || e.key === "PageDown") {
900
966
  const cur = this.activeInstance.getCurrentPage?.() ?? 1;
901
967
  this.activeInstance.goToPage?.(cur + 1);
968
+ const nextCur = this.activeInstance.getCurrentPage?.() ?? cur + 1;
969
+ this.toolbar?.setPage(nextCur);
902
970
  } else if (e.key === "ArrowLeft" || e.key === "PageUp") {
903
971
  const cur = this.activeInstance.getCurrentPage?.() ?? 1;
904
972
  this.activeInstance.goToPage?.(Math.max(1, cur - 1));
973
+ const prevCur = this.activeInstance.getCurrentPage?.() ?? Math.max(1, cur - 1);
974
+ this.toolbar?.setPage(prevCur);
905
975
  } else if (e.key === "+" || e.key === "=") {
906
976
  this.activeInstance.zoomIn?.();
907
977
  } else if (e.key === "-" || e.key === "_") {
@@ -1167,30 +1237,62 @@ var CfbfReader = class {
1167
1237
  return result.subarray(0, targetSize);
1168
1238
  }
1169
1239
  };
1170
-
1171
- // ../plugins/pdf/dist/index.js
1240
+ if (typeof window !== "undefined" && pdfjsLib.GlobalWorkerOptions) {
1241
+ if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
1242
+ pdfjsLib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/build/pdf.worker.min.mjs`;
1243
+ }
1244
+ }
1172
1245
  var PdfPlugin = class {
1173
1246
  id = "pdf";
1174
- name = "PDF Preview";
1247
+ name = "PDF Document Preview";
1175
1248
  extensions = [".pdf"];
1176
1249
  mimeTypes = ["application/pdf"];
1177
1250
  weight = 100;
1178
1251
  supports(file) {
1179
1252
  const ext = file.metadata.extension?.toLowerCase();
1180
1253
  const mime = file.metadata.mimeType?.toLowerCase();
1181
- return ext === ".pdf" || mime === "application/pdf";
1254
+ if (ext) return this.extensions.includes(ext);
1255
+ return this.mimeTypes.includes(mime || "");
1182
1256
  }
1183
1257
  getToolbarActions(instance) {
1258
+ const totalPages = instance.getPageCount?.() ?? 1;
1259
+ const curPage = instance.getCurrentPage?.() ?? 1;
1184
1260
  return [
1261
+ {
1262
+ id: "thumbnails",
1263
+ icon: "thumbnails",
1264
+ label: "Page Thumbnails",
1265
+ type: "button",
1266
+ group: "navigation",
1267
+ execute: () => instance.toggleThumbnails?.()
1268
+ },
1269
+ {
1270
+ id: "page-nav",
1271
+ icon: "",
1272
+ label: "Page Navigation",
1273
+ type: "page-nav",
1274
+ group: "navigation",
1275
+ value: curPage,
1276
+ max: totalPages,
1277
+ execute: (action, page) => {
1278
+ const cur = instance.getCurrentPage?.() ?? 1;
1279
+ const max = instance.getPageCount?.() ?? 1;
1280
+ if (action === "prev") {
1281
+ if (cur > 1) instance.goToPage?.(cur - 1);
1282
+ } else if (action === "next") {
1283
+ if (cur < max) instance.goToPage?.(cur + 1);
1284
+ } else if (typeof page === "number") {
1285
+ instance.goToPage?.(page);
1286
+ }
1287
+ }
1288
+ },
1185
1289
  {
1186
1290
  id: "zoom-out",
1187
1291
  icon: "zoom-out",
1188
1292
  label: "Zoom Out",
1189
1293
  type: "button",
1190
1294
  group: "zoom",
1191
- execute: () => {
1192
- instance.zoomOut?.();
1193
- }
1295
+ execute: () => instance.zoomOut?.()
1194
1296
  },
1195
1297
  {
1196
1298
  id: "zoom-in",
@@ -1198,9 +1300,7 @@ var PdfPlugin = class {
1198
1300
  label: "Zoom In",
1199
1301
  type: "button",
1200
1302
  group: "zoom",
1201
- execute: () => {
1202
- instance.zoomIn?.();
1203
- }
1303
+ execute: () => instance.zoomIn?.()
1204
1304
  },
1205
1305
  {
1206
1306
  id: "fit-page",
@@ -1208,39 +1308,23 @@ var PdfPlugin = class {
1208
1308
  label: "Fit to Page",
1209
1309
  type: "button",
1210
1310
  group: "zoom",
1211
- execute: () => {
1212
- instance.fitToPage?.();
1213
- }
1311
+ execute: () => instance.fitToPage?.()
1214
1312
  },
1215
1313
  {
1216
1314
  id: "rotate-cw",
1217
1315
  icon: "rotate-cw",
1218
- label: "Rotate",
1316
+ label: "Rotate Clockwise",
1219
1317
  type: "button",
1220
1318
  group: "view",
1221
- execute: () => {
1222
- instance.rotateCW?.();
1223
- }
1224
- },
1225
- {
1226
- id: "page-nav",
1227
- icon: "page-nav",
1228
- label: "Page Navigation",
1229
- type: "page-nav",
1230
- group: "navigation",
1231
- execute: (page) => {
1232
- if (typeof page === "number") instance.goToPage?.(page);
1233
- }
1319
+ execute: () => instance.rotateCW?.()
1234
1320
  },
1235
1321
  {
1236
1322
  id: "download",
1237
1323
  icon: "download",
1238
- label: "Download",
1324
+ label: "Download PDF",
1239
1325
  type: "button",
1240
1326
  group: "actions",
1241
- execute: () => {
1242
- instance.download?.();
1243
- }
1327
+ execute: () => instance.download?.()
1244
1328
  },
1245
1329
  {
1246
1330
  id: "print",
@@ -1248,99 +1332,213 @@ var PdfPlugin = class {
1248
1332
  label: "Print",
1249
1333
  type: "button",
1250
1334
  group: "actions",
1251
- execute: () => {
1252
- instance.print?.();
1253
- }
1335
+ execute: () => instance.print?.()
1254
1336
  }
1255
1337
  ];
1256
1338
  }
1257
1339
  async render(ctx) {
1258
- const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1259
- const url = URL.createObjectURL(blob);
1260
- const wrapper = document.createElement("div");
1261
- wrapper.style.width = "100%";
1262
- wrapper.style.height = "100%";
1263
- wrapper.style.overflow = "hidden";
1264
- wrapper.style.display = "flex";
1265
- wrapper.style.justifyContent = "center";
1266
- wrapper.style.alignItems = "center";
1267
- const iframe = document.createElement("iframe");
1268
- iframe.src = url;
1269
- iframe.style.width = "100%";
1270
- iframe.style.height = "100%";
1271
- iframe.style.border = "none";
1272
- wrapper.appendChild(iframe);
1273
- ctx.container.appendChild(wrapper);
1340
+ const container = document.createElement("div");
1341
+ container.className = "fp-pdf-container";
1342
+ container.style.width = "100%";
1343
+ container.style.height = "100%";
1344
+ container.style.overflow = "auto";
1345
+ container.style.display = "flex";
1346
+ container.style.flexDirection = "column";
1347
+ container.style.alignItems = "center";
1348
+ container.style.padding = "24px 16px";
1349
+ container.style.backgroundColor = "#0f172a";
1350
+ container.style.boxSizing = "border-box";
1351
+ container.style.position = "relative";
1352
+ const pageCard = document.createElement("div");
1353
+ pageCard.className = "fp-pdf-page-card";
1354
+ pageCard.style.boxShadow = "0 10px 35px rgba(0, 0, 0, 0.5)";
1355
+ pageCard.style.backgroundColor = "#ffffff";
1356
+ pageCard.style.borderRadius = "4px";
1357
+ pageCard.style.overflow = "hidden";
1358
+ pageCard.style.lineHeight = "0";
1359
+ pageCard.style.transition = "transform 0.15s ease";
1360
+ pageCard.style.position = "relative";
1361
+ const canvas = document.createElement("canvas");
1362
+ pageCard.appendChild(canvas);
1363
+ container.appendChild(pageCard);
1364
+ const indicator = document.createElement("div");
1365
+ indicator.className = "fp-pdf-page-indicator";
1366
+ indicator.style.position = "sticky";
1367
+ indicator.style.bottom = "16px";
1368
+ indicator.style.marginTop = "16px";
1369
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
1370
+ indicator.style.backdropFilter = "blur(8px)";
1371
+ indicator.style.color = "#f8fafc";
1372
+ indicator.style.fontSize = "12px";
1373
+ indicator.style.fontWeight = "600";
1374
+ indicator.style.padding = "5px 14px";
1375
+ indicator.style.borderRadius = "20px";
1376
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
1377
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
1378
+ indicator.style.zIndex = "10";
1379
+ indicator.style.userSelect = "none";
1380
+ indicator.style.pointerEvents = "none";
1381
+ container.appendChild(indicator);
1382
+ ctx.container.appendChild(container);
1383
+ const loadingTask = pdfjsLib.getDocument({
1384
+ data: new Uint8Array(ctx.buffer),
1385
+ cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/cmaps/`,
1386
+ cMapPacked: true,
1387
+ standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/standard_fonts/`
1388
+ });
1389
+ const pdfDoc = await loadingTask.promise;
1390
+ const totalPages = Math.max(1, pdfDoc.numPages);
1274
1391
  let currentPage = 1;
1275
- let currentZoom = 1;
1392
+ let zoomScale = 1;
1276
1393
  let rotation = 0;
1394
+ let currentRenderTask = null;
1395
+ const renderPage = async (pageNum) => {
1396
+ if (currentRenderTask) {
1397
+ try {
1398
+ currentRenderTask.cancel();
1399
+ } catch {
1400
+ }
1401
+ currentRenderTask = null;
1402
+ }
1403
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
1404
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
1405
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
1406
+ const page = await pdfDoc.getPage(currentPage);
1407
+ const containerWidth = container.clientWidth || 900;
1408
+ const unscaledVp = page.getViewport({ scale: 1, rotation });
1409
+ const baseScale = Math.min((containerWidth - 64) / unscaledVp.width, 1.6);
1410
+ const effectiveScale = (baseScale > 0 ? baseScale : 1) * zoomScale;
1411
+ const pixelRatio = window.devicePixelRatio || 1;
1412
+ const viewport = page.getViewport({ scale: effectiveScale, rotation });
1413
+ canvas.width = Math.floor(viewport.width * pixelRatio);
1414
+ canvas.height = Math.floor(viewport.height * pixelRatio);
1415
+ canvas.style.width = `${Math.floor(viewport.width)}px`;
1416
+ canvas.style.height = `${Math.floor(viewport.height)}px`;
1417
+ const canvasCtx = canvas.getContext("2d");
1418
+ if (!canvasCtx) return;
1419
+ canvasCtx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
1420
+ currentRenderTask = page.render({
1421
+ canvasContext: canvasCtx,
1422
+ viewport
1423
+ });
1424
+ try {
1425
+ await currentRenderTask.promise;
1426
+ } catch (err) {
1427
+ if (err?.name !== "RenderingCancelledException") {
1428
+ console.warn("[PdfPlugin] Page render warning:", err);
1429
+ }
1430
+ } finally {
1431
+ currentRenderTask = null;
1432
+ }
1433
+ };
1434
+ await renderPage(1);
1277
1435
  const cleanup = () => {
1278
- URL.revokeObjectURL(url);
1279
- wrapper.remove();
1436
+ if (currentRenderTask) {
1437
+ try {
1438
+ currentRenderTask.cancel();
1439
+ } catch {
1440
+ }
1441
+ }
1442
+ try {
1443
+ pdfDoc.destroy();
1444
+ } catch {
1445
+ }
1446
+ container.remove();
1280
1447
  ctx.container.innerHTML = "";
1281
1448
  };
1282
1449
  ctx.signal.addEventListener("abort", cleanup);
1283
- return {
1450
+ const instance = {
1284
1451
  destroy: cleanup,
1285
1452
  zoomIn: () => {
1286
- currentZoom += 0.1;
1287
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1453
+ zoomScale = Math.min(3.5, zoomScale + 0.2);
1454
+ renderPage(currentPage);
1288
1455
  },
1289
1456
  zoomOut: () => {
1290
- currentZoom = Math.max(0.2, currentZoom - 0.1);
1291
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1457
+ zoomScale = Math.max(0.3, zoomScale - 0.2);
1458
+ renderPage(currentPage);
1292
1459
  },
1293
- getZoom: () => currentZoom,
1460
+ getZoom: () => zoomScale,
1294
1461
  setZoom: (level) => {
1295
- currentZoom = level;
1296
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1462
+ zoomScale = Math.max(0.3, Math.min(3.5, level));
1463
+ renderPage(currentPage);
1297
1464
  },
1298
1465
  fitToPage: () => {
1299
- currentZoom = 1;
1300
- iframe.style.transform = `scale(1) rotate(${rotation}deg)`;
1466
+ zoomScale = 1;
1467
+ renderPage(currentPage);
1301
1468
  },
1302
1469
  rotateCW: () => {
1303
1470
  rotation = (rotation + 90) % 360;
1304
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1471
+ renderPage(currentPage);
1305
1472
  },
1306
1473
  rotateCCW: () => {
1307
1474
  rotation = (rotation - 90 + 360) % 360;
1308
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1475
+ renderPage(currentPage);
1309
1476
  },
1310
1477
  getRotation: () => rotation,
1478
+ getPageCount: () => totalPages,
1479
+ getCurrentPage: () => currentPage,
1311
1480
  goToPage: (page) => {
1312
- currentPage = page;
1313
- iframe.src = `${url}#page=${page}`;
1481
+ renderPage(page);
1314
1482
  },
1315
- getCurrentPage: () => currentPage,
1316
1483
  download: () => {
1484
+ const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1485
+ const url = URL.createObjectURL(blob);
1317
1486
  const a = document.createElement("a");
1318
1487
  a.href = url;
1319
1488
  a.download = ctx.metadata.name || "document.pdf";
1320
1489
  a.click();
1490
+ URL.revokeObjectURL(url);
1321
1491
  },
1322
1492
  print: () => {
1323
- iframe.contentWindow?.print();
1493
+ const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1494
+ const url = URL.createObjectURL(blob);
1495
+ const hiddenIframe = document.createElement("iframe");
1496
+ hiddenIframe.style.position = "fixed";
1497
+ hiddenIframe.style.right = "0";
1498
+ hiddenIframe.style.bottom = "0";
1499
+ hiddenIframe.style.width = "0";
1500
+ hiddenIframe.style.height = "0";
1501
+ hiddenIframe.style.border = "0";
1502
+ document.body.appendChild(hiddenIframe);
1503
+ hiddenIframe.src = url;
1504
+ hiddenIframe.onload = () => {
1505
+ setTimeout(() => {
1506
+ hiddenIframe.contentWindow?.print();
1507
+ setTimeout(() => {
1508
+ hiddenIframe.remove();
1509
+ URL.revokeObjectURL(url);
1510
+ }, 1e3);
1511
+ }, 300);
1512
+ };
1324
1513
  },
1325
1514
  getThumbnails: async () => {
1326
- return [
1327
- {
1328
- index: 1,
1329
- label: "Page 1",
1330
- render: async (canvas) => {
1331
- const context = canvas.getContext("2d");
1332
- if (context) {
1333
- context.fillStyle = "#fff";
1334
- context.fillRect(0, 0, canvas.width, canvas.height);
1335
- context.fillStyle = "#333";
1336
- context.font = "12px sans-serif";
1337
- context.fillText("PDF Preview", 10, 20);
1515
+ const thumbnails = [];
1516
+ const count = Math.min(totalPages, 50);
1517
+ for (let i = 1; i <= count; i++) {
1518
+ thumbnails.push({
1519
+ index: i,
1520
+ label: `Page ${i}`,
1521
+ render: async (thumbCanvas) => {
1522
+ try {
1523
+ const p = await pdfDoc.getPage(i);
1524
+ const baseVp = p.getViewport({ scale: 1 });
1525
+ const thumbScale = (thumbCanvas.width || 120) / baseVp.width;
1526
+ const thumbVp = p.getViewport({ scale: thumbScale });
1527
+ thumbCanvas.height = Math.floor(thumbVp.height);
1528
+ const tCtx = thumbCanvas.getContext("2d");
1529
+ if (tCtx) {
1530
+ await p.render({ canvasContext: tCtx, viewport: thumbVp }).promise;
1531
+ }
1532
+ } catch (e) {
1533
+ console.warn(`[PdfPlugin] Error generating thumbnail for page ${i}:`, e);
1338
1534
  }
1339
1535
  }
1340
- }
1341
- ];
1536
+ });
1537
+ }
1538
+ return thumbnails;
1342
1539
  }
1343
1540
  };
1541
+ return instance;
1344
1542
  }
1345
1543
  };
1346
1544
  function pdfPlugin() {
@@ -1661,10 +1859,37 @@ var DocxPlugin = class {
1661
1859
  supports(file) {
1662
1860
  const ext = file.metadata.extension?.toLowerCase();
1663
1861
  const mime = file.metadata.mimeType?.toLowerCase();
1664
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
1862
+ if (ext) {
1863
+ return this.extensions.includes(ext);
1864
+ }
1865
+ return this.mimeTypes.includes(mime || "");
1665
1866
  }
1666
1867
  getToolbarActions(instance) {
1667
- return [
1868
+ const totalPages = instance.getPageCount?.() ?? 1;
1869
+ const actions = [];
1870
+ if (totalPages > 1) {
1871
+ actions.push({
1872
+ id: "page-nav",
1873
+ icon: "",
1874
+ label: "Page Navigation",
1875
+ type: "page-nav",
1876
+ group: "navigation",
1877
+ value: instance.getCurrentPage?.() ?? 1,
1878
+ max: totalPages,
1879
+ execute: (action, page) => {
1880
+ const cur = instance.getCurrentPage?.() ?? 1;
1881
+ const max = instance.getPageCount?.() ?? 1;
1882
+ if (action === "prev") {
1883
+ if (cur > 1) instance.goToPage?.(cur - 1);
1884
+ } else if (action === "next") {
1885
+ if (cur < max) instance.goToPage?.(cur + 1);
1886
+ } else if (typeof page === "number") {
1887
+ instance.goToPage?.(page);
1888
+ }
1889
+ }
1890
+ });
1891
+ }
1892
+ actions.push(
1668
1893
  {
1669
1894
  id: "zoom-out",
1670
1895
  icon: "zoom-out",
@@ -1705,7 +1930,8 @@ var DocxPlugin = class {
1705
1930
  group: "actions",
1706
1931
  execute: () => instance.print?.()
1707
1932
  }
1708
- ];
1933
+ );
1934
+ return actions;
1709
1935
  }
1710
1936
  async render(ctx) {
1711
1937
  const wrapper = document.createElement("div");
@@ -1781,7 +2007,51 @@ var DocxPlugin = class {
1781
2007
  }
1782
2008
  }
1783
2009
  }
2010
+ const sections = wrapper.querySelectorAll("section.docx");
2011
+ const cards = wrapper.querySelectorAll(".fp-docx-page-card");
2012
+ const pageElements = sections.length > 0 ? sections : cards;
2013
+ const totalPages = Math.max(1, pageElements.length);
2014
+ let currentPage = 1;
2015
+ let indicator = null;
2016
+ if (totalPages > 1) {
2017
+ indicator = document.createElement("div");
2018
+ indicator.className = "fp-docx-page-indicator";
2019
+ indicator.style.position = "sticky";
2020
+ indicator.style.bottom = "16px";
2021
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
2022
+ indicator.style.backdropFilter = "blur(8px)";
2023
+ indicator.style.color = "#f8fafc";
2024
+ indicator.style.fontSize = "12px";
2025
+ indicator.style.fontWeight = "600";
2026
+ indicator.style.padding = "5px 14px";
2027
+ indicator.style.borderRadius = "20px";
2028
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
2029
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
2030
+ indicator.style.zIndex = "10";
2031
+ indicator.style.userSelect = "none";
2032
+ indicator.style.pointerEvents = "none";
2033
+ indicator.style.textAlign = "center";
2034
+ indicator.style.width = "fit-content";
2035
+ indicator.style.margin = "16px auto 0";
2036
+ ctx.container.appendChild(indicator);
2037
+ }
2038
+ const showPage = (pageNum) => {
2039
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
2040
+ if (pageElements.length > 1) {
2041
+ pageElements.forEach((sec, idx) => {
2042
+ sec.style.display = idx + 1 === currentPage ? "block" : "none";
2043
+ });
2044
+ }
2045
+ if (indicator) {
2046
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
2047
+ }
2048
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
2049
+ };
2050
+ if (totalPages > 1) {
2051
+ showPage(1);
2052
+ }
1784
2053
  const cleanup = () => {
2054
+ indicator?.remove();
1785
2055
  for (const url of createdBlobUrls) {
1786
2056
  URL.revokeObjectURL(url);
1787
2057
  }
@@ -1792,6 +2062,11 @@ var DocxPlugin = class {
1792
2062
  ctx.signal.addEventListener("abort", cleanup);
1793
2063
  return {
1794
2064
  destroy: cleanup,
2065
+ getPageCount: () => totalPages,
2066
+ getCurrentPage: () => currentPage,
2067
+ goToPage: (page) => {
2068
+ showPage(page);
2069
+ },
1795
2070
  zoomIn: () => {
1796
2071
  scale += 0.1;
1797
2072
  wrapper.style.transform = `scale(${scale})`;
@@ -2050,10 +2325,37 @@ var ExcelPlugin = class {
2050
2325
  supports(file) {
2051
2326
  const ext = file.metadata.extension?.toLowerCase();
2052
2327
  const mime = file.metadata.mimeType?.toLowerCase();
2053
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
2328
+ if (ext) {
2329
+ return this.extensions.includes(ext);
2330
+ }
2331
+ return this.mimeTypes.includes(mime || "");
2054
2332
  }
2055
2333
  getToolbarActions(instance) {
2056
- return [
2334
+ const totalSheets = instance.getPageCount?.() ?? 1;
2335
+ const actions = [];
2336
+ if (totalSheets > 1) {
2337
+ actions.push({
2338
+ id: "page-nav",
2339
+ icon: "",
2340
+ label: "Sheet Navigation",
2341
+ type: "page-nav",
2342
+ group: "navigation",
2343
+ value: instance.getCurrentPage?.() ?? 1,
2344
+ max: totalSheets,
2345
+ execute: (action, sheet) => {
2346
+ const cur = instance.getCurrentPage?.() ?? 1;
2347
+ const max = instance.getPageCount?.() ?? 1;
2348
+ if (action === "prev") {
2349
+ if (cur > 1) instance.goToPage?.(cur - 1);
2350
+ } else if (action === "next") {
2351
+ if (cur < max) instance.goToPage?.(cur + 1);
2352
+ } else if (typeof sheet === "number") {
2353
+ instance.goToPage?.(sheet);
2354
+ }
2355
+ }
2356
+ });
2357
+ }
2358
+ actions.push(
2057
2359
  {
2058
2360
  id: "zoom-out",
2059
2361
  icon: "zoom-out",
@@ -2074,16 +2376,6 @@ var ExcelPlugin = class {
2074
2376
  instance.zoomIn?.();
2075
2377
  }
2076
2378
  },
2077
- {
2078
- id: "page-nav",
2079
- icon: "page-nav",
2080
- label: "Sheet Navigation",
2081
- type: "page-nav",
2082
- group: "navigation",
2083
- execute: (sheet) => {
2084
- if (typeof sheet === "number") instance.goToPage?.(sheet);
2085
- }
2086
- },
2087
2379
  {
2088
2380
  id: "download",
2089
2381
  icon: "download",
@@ -2104,7 +2396,8 @@ var ExcelPlugin = class {
2104
2396
  instance.print?.();
2105
2397
  }
2106
2398
  }
2107
- ];
2399
+ );
2400
+ return actions;
2108
2401
  }
2109
2402
  async render(ctx) {
2110
2403
  const container = document.createElement("div");
@@ -2188,6 +2481,7 @@ var ExcelPlugin = class {
2188
2481
  b.style.fontWeight = "normal";
2189
2482
  }
2190
2483
  });
2484
+ ctx.emit("page-change", { page: currentSheetIndex, total: sheetNames.length });
2191
2485
  };
2192
2486
  if (sheetNames.length > 0) {
2193
2487
  sheetNames.forEach((name, idx) => {
@@ -2437,7 +2731,31 @@ var CodePlugin = class {
2437
2731
  return false;
2438
2732
  }
2439
2733
  getToolbarActions(instance) {
2440
- return [
2734
+ const totalPages = instance.getPageCount?.() ?? 1;
2735
+ const actions = [];
2736
+ if (totalPages > 1) {
2737
+ actions.push({
2738
+ id: "page-nav",
2739
+ icon: "",
2740
+ label: "Page Navigation",
2741
+ type: "page-nav",
2742
+ group: "navigation",
2743
+ value: instance.getCurrentPage?.() ?? 1,
2744
+ max: totalPages,
2745
+ execute: (action, page) => {
2746
+ const cur = instance.getCurrentPage?.() ?? 1;
2747
+ const max = instance.getPageCount?.() ?? 1;
2748
+ if (action === "prev") {
2749
+ if (cur > 1) instance.goToPage?.(cur - 1);
2750
+ } else if (action === "next") {
2751
+ if (cur < max) instance.goToPage?.(cur + 1);
2752
+ } else if (typeof page === "number") {
2753
+ instance.goToPage?.(page);
2754
+ }
2755
+ }
2756
+ });
2757
+ }
2758
+ actions.push(
2441
2759
  {
2442
2760
  id: "zoom-out",
2443
2761
  icon: "zoom-out",
@@ -2488,11 +2806,16 @@ var CodePlugin = class {
2488
2806
  instance.print?.();
2489
2807
  }
2490
2808
  }
2491
- ];
2809
+ );
2810
+ return actions;
2492
2811
  }
2493
2812
  async render(ctx) {
2494
2813
  const decoder = new TextDecoder("utf-8");
2495
- const text = decoder.decode(ctx.buffer);
2814
+ const fullText = decoder.decode(ctx.buffer);
2815
+ const pageSplitRegex = /(?:\f|\x0C|(?:\r?\n|^)\s*[-=_]{3,}\s*(?:PAGE|Page|page break|Page Break)[\s\d\w-]*[-=_]{3,}\s*(?:\r?\n|$))/i;
2816
+ const rawPages = fullText.split(pageSplitRegex).map((p) => p.trim()).filter((p) => p.length > 0);
2817
+ const totalPages = Math.max(1, rawPages.length);
2818
+ let currentPage = 1;
2496
2819
  const container = document.createElement("div");
2497
2820
  container.style.width = "100%";
2498
2821
  container.style.height = "100%";
@@ -2511,25 +2834,66 @@ var CodePlugin = class {
2511
2834
  pre.style.wordBreak = "break-all";
2512
2835
  const code = document.createElement("code");
2513
2836
  const ext = (ctx.metadata.extension || "").replace(".", "");
2514
- try {
2515
- if (ext && hljs.getLanguage(ext)) {
2516
- code.innerHTML = hljs.highlight(text, { language: ext }).value;
2517
- } else {
2518
- code.innerHTML = hljs.highlightAuto(text).value;
2837
+ const renderCodePage = (text) => {
2838
+ try {
2839
+ if (ext && hljs.getLanguage(ext)) {
2840
+ code.innerHTML = hljs.highlight(text, { language: ext }).value;
2841
+ } else {
2842
+ code.innerHTML = hljs.highlightAuto(text).value;
2843
+ }
2844
+ } catch {
2845
+ code.textContent = text;
2519
2846
  }
2520
- } catch {
2521
- code.textContent = text;
2522
- }
2847
+ };
2848
+ renderCodePage(rawPages[0] || fullText);
2523
2849
  pre.appendChild(code);
2524
2850
  container.appendChild(pre);
2851
+ let indicator = null;
2852
+ if (totalPages > 1) {
2853
+ indicator = document.createElement("div");
2854
+ indicator.className = "fp-code-page-indicator";
2855
+ indicator.style.position = "sticky";
2856
+ indicator.style.bottom = "16px";
2857
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
2858
+ indicator.style.backdropFilter = "blur(8px)";
2859
+ indicator.style.color = "#f8fafc";
2860
+ indicator.style.fontSize = "12px";
2861
+ indicator.style.fontWeight = "600";
2862
+ indicator.style.padding = "5px 14px";
2863
+ indicator.style.borderRadius = "20px";
2864
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
2865
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
2866
+ indicator.style.zIndex = "10";
2867
+ indicator.style.userSelect = "none";
2868
+ indicator.style.pointerEvents = "none";
2869
+ indicator.style.textAlign = "center";
2870
+ indicator.style.width = "fit-content";
2871
+ indicator.style.margin = "16px auto 0";
2872
+ container.appendChild(indicator);
2873
+ }
2874
+ const showPage = (pageNum) => {
2875
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
2876
+ renderCodePage(rawPages[currentPage - 1] || fullText);
2877
+ if (indicator) {
2878
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
2879
+ }
2880
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
2881
+ };
2882
+ if (totalPages > 1) {
2883
+ showPage(1);
2884
+ }
2525
2885
  ctx.container.appendChild(container);
2526
2886
  const cleanup = () => {
2887
+ indicator?.remove();
2527
2888
  container.remove();
2528
2889
  ctx.container.innerHTML = "";
2529
2890
  };
2530
2891
  ctx.signal.addEventListener("abort", cleanup);
2531
2892
  return {
2532
2893
  destroy: cleanup,
2894
+ getPageCount: () => totalPages,
2895
+ getCurrentPage: () => currentPage,
2896
+ goToPage: (page) => showPage(page),
2533
2897
  zoomIn: () => {
2534
2898
  fontSize = Math.min(32, fontSize + 2);
2535
2899
  pre.style.fontSize = `${fontSize}px`;
@@ -2557,7 +2921,7 @@ var CodePlugin = class {
2557
2921
  window.print();
2558
2922
  },
2559
2923
  copy: () => {
2560
- navigator.clipboard?.writeText(text);
2924
+ navigator.clipboard?.writeText(fullText);
2561
2925
  }
2562
2926
  };
2563
2927
  }
@@ -2992,7 +3356,10 @@ var PptxPlugin = class {
2992
3356
  supports(file) {
2993
3357
  const ext = file.metadata.extension?.toLowerCase();
2994
3358
  const mime = file.metadata.mimeType?.toLowerCase();
2995
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3359
+ if (ext) {
3360
+ return this.extensions.includes(ext);
3361
+ }
3362
+ return this.mimeTypes.includes(mime || "");
2996
3363
  }
2997
3364
  getToolbarActions(instance) {
2998
3365
  return [
@@ -3376,7 +3743,31 @@ var RtfPlugin = class {
3376
3743
  return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3377
3744
  }
3378
3745
  getToolbarActions(instance) {
3379
- return [
3746
+ const totalPages = instance.getPageCount?.() ?? 1;
3747
+ const actions = [];
3748
+ if (totalPages > 1) {
3749
+ actions.push({
3750
+ id: "page-nav",
3751
+ icon: "",
3752
+ label: "Page Navigation",
3753
+ type: "page-nav",
3754
+ group: "navigation",
3755
+ value: instance.getCurrentPage?.() ?? 1,
3756
+ max: totalPages,
3757
+ execute: (action, page) => {
3758
+ const cur = instance.getCurrentPage?.() ?? 1;
3759
+ const max = instance.getPageCount?.() ?? 1;
3760
+ if (action === "prev") {
3761
+ if (cur > 1) instance.goToPage?.(cur - 1);
3762
+ } else if (action === "next") {
3763
+ if (cur < max) instance.goToPage?.(cur + 1);
3764
+ } else if (typeof page === "number") {
3765
+ instance.goToPage?.(page);
3766
+ }
3767
+ }
3768
+ });
3769
+ }
3770
+ actions.push(
3380
3771
  {
3381
3772
  id: "zoom-out",
3382
3773
  icon: "zoom-out",
@@ -3417,7 +3808,8 @@ var RtfPlugin = class {
3417
3808
  group: "actions",
3418
3809
  execute: () => instance.print?.()
3419
3810
  }
3420
- ];
3811
+ );
3812
+ return actions;
3421
3813
  }
3422
3814
  async render(ctx) {
3423
3815
  const wrapper = document.createElement("div");
@@ -3436,25 +3828,82 @@ var RtfPlugin = class {
3436
3828
  ctx.container.style.backgroundColor = "#f1f5f9";
3437
3829
  ctx.container.appendChild(wrapper);
3438
3830
  let scale = 1;
3831
+ let pageElements = [];
3439
3832
  try {
3833
+ if (typeof RTFJS.loggingEnabled === "function") {
3834
+ RTFJS.loggingEnabled(false);
3835
+ }
3440
3836
  const doc = new RTFJS.Document(ctx.buffer, {});
3441
3837
  const htmlElements = await doc.render();
3442
- for (const el of htmlElements) {
3838
+ pageElements = htmlElements;
3839
+ for (let i = 0; i < htmlElements.length; i++) {
3840
+ const el = htmlElements[i];
3841
+ el.style.display = i === 0 ? "block" : "none";
3443
3842
  wrapper.appendChild(el);
3444
3843
  }
3445
3844
  } catch (err) {
3446
3845
  console.warn("[RtfPlugin] RTF render error, fallback text:", err);
3447
3846
  const text = new TextDecoder("latin1").decode(ctx.buffer);
3448
3847
  const clean = text.replace(/\\par[d]?/g, "\n").replace(/\\[a-zA-Z0-9\-]+/g, "").replace(/[{}]/g, "");
3449
- wrapper.innerHTML = `<pre style="white-space: pre-wrap; font-family: serif; color: #333;">${clean}</pre>`;
3848
+ const pre = document.createElement("pre");
3849
+ pre.style.whiteSpace = "pre-wrap";
3850
+ pre.style.fontFamily = "serif";
3851
+ pre.style.color = "#333";
3852
+ pre.textContent = clean;
3853
+ wrapper.appendChild(pre);
3854
+ pageElements = [pre];
3855
+ }
3856
+ const totalPages = Math.max(1, pageElements.length);
3857
+ let currentPage = 1;
3858
+ let indicator = null;
3859
+ if (totalPages > 1) {
3860
+ indicator = document.createElement("div");
3861
+ indicator.className = "fp-rtf-page-indicator";
3862
+ indicator.style.position = "sticky";
3863
+ indicator.style.bottom = "16px";
3864
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
3865
+ indicator.style.backdropFilter = "blur(8px)";
3866
+ indicator.style.color = "#f8fafc";
3867
+ indicator.style.fontSize = "12px";
3868
+ indicator.style.fontWeight = "600";
3869
+ indicator.style.padding = "5px 14px";
3870
+ indicator.style.borderRadius = "20px";
3871
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
3872
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
3873
+ indicator.style.zIndex = "10";
3874
+ indicator.style.userSelect = "none";
3875
+ indicator.style.pointerEvents = "none";
3876
+ indicator.style.textAlign = "center";
3877
+ indicator.style.width = "fit-content";
3878
+ indicator.style.margin = "16px auto 0";
3879
+ ctx.container.appendChild(indicator);
3880
+ }
3881
+ const showPage = (pageNum) => {
3882
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
3883
+ if (totalPages > 1) {
3884
+ pageElements.forEach((el, idx) => {
3885
+ el.style.display = idx + 1 === currentPage ? "block" : "none";
3886
+ });
3887
+ }
3888
+ if (indicator) {
3889
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
3890
+ }
3891
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
3892
+ };
3893
+ if (totalPages > 1) {
3894
+ showPage(1);
3450
3895
  }
3451
3896
  const cleanup = () => {
3897
+ indicator?.remove();
3452
3898
  wrapper.remove();
3453
3899
  ctx.container.innerHTML = "";
3454
3900
  };
3455
3901
  ctx.signal.addEventListener("abort", cleanup);
3456
3902
  return {
3457
3903
  destroy: cleanup,
3904
+ getPageCount: () => totalPages,
3905
+ getCurrentPage: () => currentPage,
3906
+ goToPage: (page) => showPage(page),
3458
3907
  zoomIn: () => {
3459
3908
  scale += 0.1;
3460
3909
  wrapper.style.transform = `scale(${scale})`;
@@ -3635,45 +4084,48 @@ var OpenDocumentPlugin = class {
3635
4084
  supports(file) {
3636
4085
  const ext = file.metadata.extension?.toLowerCase();
3637
4086
  const mime = file.metadata.mimeType?.toLowerCase();
3638
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4087
+ if (ext) {
4088
+ return this.extensions.includes(ext);
4089
+ }
4090
+ return this.mimeTypes.includes(mime || "");
3639
4091
  }
3640
4092
  getToolbarActions(instance) {
3641
4093
  const isPresentation = instance.isPresentation;
4094
+ const totalPages = instance.getPageCount?.() ?? 1;
3642
4095
  const actions = [];
3643
- if (isPresentation) {
3644
- actions.push(
3645
- {
4096
+ if (isPresentation || totalPages > 1) {
4097
+ if (isPresentation) {
4098
+ actions.push({
3646
4099
  id: "thumbnails",
3647
4100
  icon: "thumbnails",
3648
4101
  label: "Slide Thumbnails",
3649
4102
  type: "button",
3650
4103
  group: "navigation",
3651
4104
  execute: () => instance.toggleThumbnails?.()
3652
- },
3653
- {
3654
- id: "page-nav",
3655
- icon: "",
3656
- label: "Slide Navigation",
3657
- type: "page-nav",
3658
- group: "navigation",
3659
- value: instance.getCurrentPage?.() ?? 1,
3660
- max: instance.getPageCount?.() ?? 1,
3661
- execute: (action, page) => {
3662
- if (action === "prev") {
3663
- const cur = instance.getCurrentPage?.() ?? 1;
3664
- if (cur > 1) instance.goToPage?.(cur - 1);
3665
- } else if (action === "next") {
3666
- const cur = instance.getCurrentPage?.() ?? 1;
3667
- const total = instance.getPageCount?.() ?? 1;
3668
- if (cur < total) instance.goToPage?.(cur + 1);
3669
- } else if (typeof page === "number") {
3670
- instance.goToPage?.(page);
3671
- } else if (typeof action === "number") {
3672
- instance.goToPage?.(action);
3673
- }
4105
+ });
4106
+ }
4107
+ actions.push({
4108
+ id: "page-nav",
4109
+ icon: "",
4110
+ label: isPresentation ? "Slide Navigation" : "Page Navigation",
4111
+ type: "page-nav",
4112
+ group: "navigation",
4113
+ value: instance.getCurrentPage?.() ?? 1,
4114
+ max: totalPages,
4115
+ execute: (action, page) => {
4116
+ const cur = instance.getCurrentPage?.() ?? 1;
4117
+ const max = instance.getPageCount?.() ?? 1;
4118
+ if (action === "prev") {
4119
+ if (cur > 1) instance.goToPage?.(cur - 1);
4120
+ } else if (action === "next") {
4121
+ if (cur < max) instance.goToPage?.(cur + 1);
4122
+ } else if (typeof page === "number") {
4123
+ instance.goToPage?.(page);
4124
+ } else if (typeof action === "number") {
4125
+ instance.goToPage?.(action);
3674
4126
  }
3675
4127
  }
3676
- );
4128
+ });
3677
4129
  }
3678
4130
  actions.push(
3679
4131
  {
@@ -4050,10 +4502,37 @@ var DocPlugin = class {
4050
4502
  supports(file) {
4051
4503
  const ext = file.metadata.extension?.toLowerCase();
4052
4504
  const mime = file.metadata.mimeType?.toLowerCase();
4053
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4505
+ if (ext) {
4506
+ return this.extensions.includes(ext);
4507
+ }
4508
+ return this.mimeTypes.includes(mime || "");
4054
4509
  }
4055
4510
  getToolbarActions(instance) {
4056
- return [
4511
+ const totalPages = instance.getPageCount?.() ?? 1;
4512
+ const actions = [];
4513
+ if (totalPages > 1) {
4514
+ actions.push({
4515
+ id: "page-nav",
4516
+ icon: "",
4517
+ label: "Page Navigation",
4518
+ type: "page-nav",
4519
+ group: "navigation",
4520
+ value: instance.getCurrentPage?.() ?? 1,
4521
+ max: totalPages,
4522
+ execute: (action, page) => {
4523
+ const cur = instance.getCurrentPage?.() ?? 1;
4524
+ const max = instance.getPageCount?.() ?? 1;
4525
+ if (action === "prev") {
4526
+ if (cur > 1) instance.goToPage?.(cur - 1);
4527
+ } else if (action === "next") {
4528
+ if (cur < max) instance.goToPage?.(cur + 1);
4529
+ } else if (typeof page === "number") {
4530
+ instance.goToPage?.(page);
4531
+ }
4532
+ }
4533
+ });
4534
+ }
4535
+ actions.push(
4057
4536
  {
4058
4537
  id: "zoom-out",
4059
4538
  icon: "zoom-out",
@@ -4102,7 +4581,8 @@ var DocPlugin = class {
4102
4581
  group: "actions",
4103
4582
  execute: () => instance.print?.()
4104
4583
  }
4105
- ];
4584
+ );
4585
+ return actions;
4106
4586
  }
4107
4587
  async render(ctx) {
4108
4588
  const container = document.createElement("div");
@@ -4129,6 +4609,7 @@ var DocPlugin = class {
4129
4609
  ctx.container.appendChild(container);
4130
4610
  let scale = 1;
4131
4611
  let extractedRawText = "";
4612
+ let isFallback = false;
4132
4613
  try {
4133
4614
  const cfbf = new CfbfReader(ctx.buffer);
4134
4615
  const wordDocStream = cfbf.readStream("WordDocument");
@@ -4142,26 +4623,89 @@ var DocPlugin = class {
4142
4623
  const tableStream = cfbf.readStream(tableName);
4143
4624
  const text = this.extractDocText(wordDocStream, tableStream);
4144
4625
  extractedRawText = text;
4145
- wrapper.innerHTML = this.formatDocToHtml(text, ctx.metadata.name || "Document");
4146
4626
  } catch (err) {
4147
4627
  console.warn("[DocPlugin] Binary parsing error, fallback text:", err);
4148
4628
  const fallback = this.heuristicTextExtraction(ctx.buffer);
4149
4629
  extractedRawText = fallback;
4150
- wrapper.innerHTML = `
4151
- <div style="border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 24px;">
4152
- <h2 style="margin: 0 0 6px; font-size: 20px; color: #334155;">${ctx.metadata.name || "Word Document (.doc)"}</h2>
4153
- <span style="font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px;">Legacy Word 97-2003 Binary Preview</span>
4154
- </div>
4155
- ${this.formatDocToHtml(fallback, ctx.metadata.name || "Document")}
4156
- `;
4630
+ isFallback = true;
4631
+ }
4632
+ const rawPages = this.splitIntoPages(extractedRawText);
4633
+ const totalPages = Math.max(1, rawPages.length);
4634
+ let currentPage = 1;
4635
+ wrapper.innerHTML = "";
4636
+ const pageCards = [];
4637
+ for (let i = 0; i < totalPages; i++) {
4638
+ const pageCard = document.createElement("div");
4639
+ pageCard.className = "fp-doc-page-card";
4640
+ pageCard.style.backgroundColor = "#ffffff";
4641
+ pageCard.style.boxShadow = "0 2px 12px rgba(0,0,0,0.08)";
4642
+ pageCard.style.borderRadius = "4px";
4643
+ pageCard.style.padding = "56px 48px";
4644
+ pageCard.style.minHeight = "100%";
4645
+ pageCard.style.display = i === 0 ? "block" : "none";
4646
+ if (isFallback && i === 0) {
4647
+ pageCard.innerHTML = `
4648
+ <div style="border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 24px;">
4649
+ <h2 style="margin: 0 0 6px; font-size: 20px; color: #334155;">${DOMPurify2.sanitize(ctx.metadata.name || "Word Document (.doc)")}</h2>
4650
+ <span style="font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px;">Legacy Word 97-2003 Binary Preview</span>
4651
+ </div>
4652
+ ${this.formatDocToHtml(rawPages[i], ctx.metadata.name || "Document")}
4653
+ `;
4654
+ } else {
4655
+ pageCard.innerHTML = this.formatDocToHtml(rawPages[i], ctx.metadata.name || "Document");
4656
+ }
4657
+ wrapper.appendChild(pageCard);
4658
+ pageCards.push(pageCard);
4659
+ }
4660
+ let indicator = null;
4661
+ if (totalPages > 1) {
4662
+ indicator = document.createElement("div");
4663
+ indicator.className = "fp-doc-page-indicator";
4664
+ indicator.style.position = "sticky";
4665
+ indicator.style.bottom = "16px";
4666
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
4667
+ indicator.style.backdropFilter = "blur(8px)";
4668
+ indicator.style.color = "#f8fafc";
4669
+ indicator.style.fontSize = "12px";
4670
+ indicator.style.fontWeight = "600";
4671
+ indicator.style.padding = "5px 14px";
4672
+ indicator.style.borderRadius = "20px";
4673
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
4674
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
4675
+ indicator.style.zIndex = "10";
4676
+ indicator.style.userSelect = "none";
4677
+ indicator.style.pointerEvents = "none";
4678
+ indicator.style.textAlign = "center";
4679
+ indicator.style.width = "fit-content";
4680
+ indicator.style.margin = "16px auto 0";
4681
+ container.appendChild(indicator);
4682
+ }
4683
+ const showPage = (pageNum) => {
4684
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
4685
+ if (totalPages > 1) {
4686
+ pageCards.forEach((card, idx) => {
4687
+ card.style.display = idx + 1 === currentPage ? "block" : "none";
4688
+ });
4689
+ }
4690
+ if (indicator) {
4691
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
4692
+ }
4693
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
4694
+ };
4695
+ if (totalPages > 1) {
4696
+ showPage(1);
4157
4697
  }
4158
4698
  const cleanup = () => {
4699
+ indicator?.remove();
4159
4700
  container.remove();
4160
4701
  ctx.container.innerHTML = "";
4161
4702
  };
4162
4703
  ctx.signal.addEventListener("abort", cleanup);
4163
4704
  return {
4164
4705
  destroy: cleanup,
4706
+ getPageCount: () => totalPages,
4707
+ getCurrentPage: () => currentPage,
4708
+ goToPage: (page) => showPage(page),
4165
4709
  zoomIn: () => {
4166
4710
  scale += 0.1;
4167
4711
  wrapper.style.transform = `scale(${scale})`;
@@ -4273,31 +4817,36 @@ var DocPlugin = class {
4273
4817
  * Scans a byte array for continuous sequences of readable characters (ANSI and UTF-16LE)
4274
4818
  */
4275
4819
  extractStringsFromBytes(bytes) {
4276
- const chars = [];
4277
- const len = bytes.length;
4278
- for (let i = 0; i < len; i++) {
4279
- const b = bytes[i];
4280
- if (b === 13 || b === 10 || b === 9 || b >= 32 && b <= 126 || b >= 160 && b <= 255) {
4281
- chars.push(String.fromCharCode(b));
4282
- } else if (b === 0 && i + 1 < len && bytes[i + 1] >= 32 && bytes[i + 1] <= 126) {
4283
- chars.push(String.fromCharCode(bytes[i + 1]));
4284
- i++;
4285
- } else if (b === 7) {
4286
- chars.push(" ");
4287
- } else if (b === 12) {
4288
- chars.push("\n\n---PAGE---\n\n");
4820
+ const rawAnsi = new TextDecoder("latin1").decode(bytes);
4821
+ const rawUtf16 = new TextDecoder("utf-16le", { fatal: false }).decode(bytes);
4822
+ const ansiRuns = rawAnsi.match(/[\x20-\x7E\t\r\n]{4,}/g) || [];
4823
+ const utf16Runs = rawUtf16.match(/[\x20-\x7E\t\r\n]{4,}/g) || [];
4824
+ const candidateLines = [];
4825
+ const seen = /* @__PURE__ */ new Set();
4826
+ for (const run of [...ansiRuns, ...utf16Runs]) {
4827
+ const trimmed = run.trim();
4828
+ if (trimmed.length >= 4 && /[a-zA-Z]/.test(trimmed) && !seen.has(trimmed)) {
4829
+ 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)) {
4830
+ seen.add(trimmed);
4831
+ candidateLines.push(trimmed);
4832
+ }
4289
4833
  }
4290
4834
  }
4291
- return chars.join("");
4835
+ return candidateLines.join("\n\n");
4292
4836
  }
4293
4837
  heuristicTextExtraction(buffer) {
4294
4838
  return this.extractStringsFromBytes(new Uint8Array(buffer));
4295
4839
  }
4840
+ splitIntoPages(text) {
4841
+ if (!text) return [""];
4842
+ 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);
4843
+ return parts.length > 0 ? parts : [text];
4844
+ }
4296
4845
  /**
4297
4846
  * Format cleaned extracted text into attractive HTML paragraphs, headings, and lists
4298
4847
  */
4299
4848
  formatDocToHtml(text, filename) {
4300
- const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x0C/g, "\n\n").split("\n");
4849
+ const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").split("\n");
4301
4850
  let html = "";
4302
4851
  let inList = false;
4303
4852
  for (const rawLine of lines) {
@@ -4343,14 +4892,17 @@ function docPlugin() {
4343
4892
  }
4344
4893
  var PptPlugin = class {
4345
4894
  id = "ppt";
4346
- name = "Legacy PowerPoint Presentation (.ppt, .pps, .pot)";
4895
+ name = "PowerPoint Presentation (.ppt, .pps, .pot)";
4347
4896
  extensions = [".ppt", ".pps", ".pot"];
4348
4897
  mimeTypes = ["application/vnd.ms-powerpoint"];
4349
- weight = 75;
4898
+ weight = 85;
4350
4899
  supports(file) {
4351
4900
  const ext = file.metadata.extension?.toLowerCase();
4352
4901
  const mime = file.metadata.mimeType?.toLowerCase();
4353
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4902
+ if (ext) {
4903
+ return this.extensions.includes(ext);
4904
+ }
4905
+ return this.mimeTypes.includes(mime || "");
4354
4906
  }
4355
4907
  getToolbarActions(instance) {
4356
4908
  return [
@@ -4438,19 +4990,15 @@ var PptPlugin = class {
4438
4990
  container.style.alignItems = "center";
4439
4991
  container.style.padding = "32px 16px";
4440
4992
  container.style.backgroundColor = "#0f172a";
4993
+ container.style.boxSizing = "border-box";
4441
4994
  const slideCard = document.createElement("div");
4442
4995
  slideCard.className = "fp-ppt-slide-card";
4443
4996
  slideCard.style.width = "960px";
4444
- slideCard.style.maxWidth = "90%";
4997
+ slideCard.style.maxWidth = "92%";
4445
4998
  slideCard.style.aspectRatio = "16 / 9";
4446
4999
  slideCard.style.backgroundColor = "#ffffff";
4447
- slideCard.style.boxShadow = "0 8px 30px rgba(0,0,0,0.3)";
5000
+ slideCard.style.boxShadow = "0 12px 40px rgba(0,0,0,0.35)";
4448
5001
  slideCard.style.borderRadius = "8px";
4449
- slideCard.style.padding = "48px";
4450
- slideCard.style.display = "flex";
4451
- slideCard.style.flexDirection = "column";
4452
- slideCard.style.justifyContent = "center";
4453
- slideCard.style.alignItems = "center";
4454
5002
  slideCard.style.boxSizing = "border-box";
4455
5003
  slideCard.style.position = "relative";
4456
5004
  slideCard.style.overflow = "hidden";
@@ -4461,21 +5009,25 @@ var PptPlugin = class {
4461
5009
  let scale = 1;
4462
5010
  let currentSlide = 1;
4463
5011
  let slides = [];
5012
+ const createdBlobUrls = [];
4464
5013
  try {
4465
5014
  const cfbf = new CfbfReader(ctx.buffer);
4466
5015
  const pptStream = cfbf.readStream("PowerPoint Document");
4467
5016
  if (!pptStream || pptStream.length < 512) {
4468
5017
  throw new Error("PowerPoint Document stream not found in CFBF container");
4469
5018
  }
4470
- slides = this.extractSlides(pptStream);
5019
+ const pictures = this.extractPictures(cfbf, createdBlobUrls);
5020
+ slides = this.extractSlides(pptStream, pictures);
4471
5021
  } catch (err) {
4472
5022
  console.warn("[PptPlugin] Error extracting binary slides:", err);
4473
5023
  }
4474
5024
  if (slides.length === 0) {
4475
5025
  slides = [
4476
5026
  {
5027
+ slideIndex: 1,
4477
5028
  title: ctx.metadata.name || "PowerPoint Presentation",
4478
- texts: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"]
5029
+ paragraphs: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"],
5030
+ tableColumns: []
4479
5031
  }
4480
5032
  ];
4481
5033
  }
@@ -4484,23 +5036,73 @@ var PptPlugin = class {
4484
5036
  currentSlide = idx;
4485
5037
  const s = slides[idx - 1];
4486
5038
  if (!s) return;
5039
+ let contentHtml = "";
5040
+ if (s.pictureUrl) {
5041
+ contentHtml = `
5042
+ <div style="flex: 1; display: flex; justify-content: center; align-items: center; padding: 12px; overflow: hidden;">
5043
+ <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;" />
5044
+ </div>
5045
+ `;
5046
+ } else if (s.tableColumns.length > 0) {
5047
+ const cols = s.tableColumns;
5048
+ contentHtml = `
5049
+ <div style="flex: 1; overflow: auto; padding: 8px 0;">
5050
+ <table style="width: 100%; border-collapse: collapse; border: 1px solid #cbd5e1; border-radius: 6px; overflow: hidden; background: #ffffff;">
5051
+ <thead>
5052
+ <tr style="background: #e2e8f0; color: #1e293b; font-weight: 600; font-size: 14px;">
5053
+ ${cols.map((c) => `<th style="padding: 12px 16px; border: 1px solid #cbd5e1; text-align: left;">${DOMPurify2.sanitize(c)}</th>`).join("")}
5054
+ </tr>
5055
+ </thead>
5056
+ <tbody>
5057
+ ${[1, 2, 3, 4, 5].map((rowIdx) => `
5058
+ <tr style="${rowIdx % 2 === 0 ? "background: #f8fafc;" : "background: #ffffff;"}">
5059
+ ${cols.map((_, cIdx) => `<td style="padding: 10px 16px; border: 1px solid #e2e8f0; font-size: 13px; color: #334155;">Data ${rowIdx}-${cIdx + 1}</td>`).join("")}
5060
+ </tr>
5061
+ `).join("")}
5062
+ </tbody>
5063
+ </table>
5064
+ </div>
5065
+ `;
5066
+ } else {
5067
+ const pTags = s.paragraphs.map((p) => {
5068
+ const lines = p.split(/[\r\n]+/).map((l) => l.trim()).filter(Boolean);
5069
+ 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("");
5070
+ }).join("");
5071
+ contentHtml = `
5072
+ <div style="flex: 1; overflow: auto; padding: 4px 8px; display: flex; flex-direction: column; justify-content: flex-start;">
5073
+ ${pTags || '<p style="color: #64748b; font-style: italic;">No additional text on this slide</p>'}
5074
+ </div>
5075
+ `;
5076
+ }
4487
5077
  slideCard.innerHTML = `
4488
- <div style="position: absolute; top: 20px; right: 24px; font-size: 12px; color: #94a3b8; font-weight: 600;">
4489
- Slide ${idx} of ${totalSlides}
4490
- </div>
4491
- <div style="text-align: center; width: 100%;">
4492
- <h1 style="font-size: ${idx === 1 ? "36px" : "28px"}; color: #1e3a8a; margin: 0 0 24px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-weight: 700;">
4493
- ${DOMPurify2.sanitize(s.title || `Slide ${idx}`)}
4494
- </h1>
4495
- <div style="display: flex; flex-direction: column; gap: 12px; max-width: 80%; margin: 0 auto; text-align: ${idx === 1 ? "center" : "left"};">
4496
- ${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("")}
5078
+ <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;">
5079
+ <!-- Header Banner matching PowerPoint design -->
5080
+ <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;">
5081
+ <h1 style="margin: 0; font-size: 26px; font-weight: 700; color: #1e293b; letter-spacing: -0.3px;">
5082
+ ${DOMPurify2.sanitize(s.title)}
5083
+ </h1>
5084
+ ${s.subtitle ? `
5085
+ <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);">
5086
+ ${DOMPurify2.sanitize(s.subtitle)}
5087
+ </span>
5088
+ ` : `
5089
+ <span style="font-size: 12px; color: #365314; font-weight: 600;">
5090
+ Slide ${idx} / ${totalSlides}
5091
+ </span>
5092
+ `}
4497
5093
  </div>
5094
+
5095
+ <!-- Slide Content -->
5096
+ ${contentHtml}
4498
5097
  </div>
4499
5098
  `;
4500
5099
  ctx.emit("page-change", { page: currentSlide, total: totalSlides });
4501
5100
  };
4502
5101
  renderSlide(1);
4503
5102
  const cleanup = () => {
5103
+ for (const u of createdBlobUrls) {
5104
+ URL.revokeObjectURL(u);
5105
+ }
4504
5106
  container.remove();
4505
5107
  ctx.container.innerHTML = "";
4506
5108
  };
@@ -4540,13 +5142,48 @@ var PptPlugin = class {
4540
5142
  if (!ctx2d) return;
4541
5143
  canvas.width = 160;
4542
5144
  canvas.height = 90;
4543
- ctx2d.fillStyle = "#ffffff";
5145
+ ctx2d.fillStyle = "#334155";
4544
5146
  ctx2d.fillRect(0, 0, 160, 90);
4545
- ctx2d.fillStyle = "#1e3a8a";
4546
- ctx2d.font = "bold 11px sans-serif";
4547
- ctx2d.textAlign = "center";
4548
- const title = s.title.slice(0, 18) || `Slide ${idx + 1}`;
4549
- ctx2d.fillText(title, 80, 50);
5147
+ ctx2d.fillStyle = "#ffffff";
5148
+ ctx2d.fillRect(3, 3, 154, 84);
5149
+ ctx2d.fillStyle = "#84cc16";
5150
+ ctx2d.fillRect(6, 6, 148, 18);
5151
+ ctx2d.fillStyle = "#1e293b";
5152
+ ctx2d.font = "bold 9px sans-serif";
5153
+ ctx2d.textAlign = "left";
5154
+ const displayTitle = s.title.length > 18 ? s.title.slice(0, 16) + ".." : s.title;
5155
+ ctx2d.fillText(displayTitle, 10, 19);
5156
+ if (s.subtitle) {
5157
+ ctx2d.fillStyle = "#38bdf8";
5158
+ ctx2d.fillRect(116, 9, 34, 12);
5159
+ ctx2d.fillStyle = "#ffffff";
5160
+ ctx2d.font = "bold 7px sans-serif";
5161
+ ctx2d.textAlign = "center";
5162
+ ctx2d.fillText(s.subtitle.slice(0, 7), 133, 18);
5163
+ }
5164
+ if (s.pictureUrl) {
5165
+ ctx2d.fillStyle = "#3b82f6";
5166
+ ctx2d.fillRect(52, 34, 56, 42);
5167
+ ctx2d.fillStyle = "#ffffff";
5168
+ ctx2d.font = "8px sans-serif";
5169
+ ctx2d.textAlign = "center";
5170
+ ctx2d.fillText("Chart", 80, 58);
5171
+ } else if (s.tableColumns.length > 0) {
5172
+ ctx2d.strokeStyle = "#cbd5e1";
5173
+ ctx2d.lineWidth = 1;
5174
+ ctx2d.strokeRect(14, 32, 132, 46);
5175
+ for (let l = 1; l <= 3; l++) {
5176
+ ctx2d.beginPath();
5177
+ ctx2d.moveTo(14, 32 + l * 11);
5178
+ ctx2d.lineTo(146, 32 + l * 11);
5179
+ ctx2d.stroke();
5180
+ }
5181
+ } else {
5182
+ ctx2d.fillStyle = "#94a3b8";
5183
+ for (let l = 0; l < 4; l++) {
5184
+ ctx2d.fillRect(14, 34 + l * 10, 132 - l * 14, 4);
5185
+ }
5186
+ }
4550
5187
  }
4551
5188
  }));
4552
5189
  },
@@ -4564,66 +5201,165 @@ var PptPlugin = class {
4564
5201
  }
4565
5202
  };
4566
5203
  }
5204
+ /**
5205
+ * Extract PNG and JPEG images from the Pictures stream
5206
+ */
5207
+ extractPictures(cfbf, createdUrls) {
5208
+ const urls = [];
5209
+ try {
5210
+ const picStream = cfbf.readStream("Pictures");
5211
+ if (!picStream || picStream.length < 32) return urls;
5212
+ const pBuf = new Uint8Array(picStream);
5213
+ const pngSig = [137, 80, 78, 71, 13, 10, 26, 10];
5214
+ const iendSig = [73, 69, 78, 68, 174, 66, 96, 130];
5215
+ for (let i = 0; i <= pBuf.length - 8; i++) {
5216
+ let match = true;
5217
+ for (let j = 0; j < 8; j++) {
5218
+ if (pBuf[i + j] !== pngSig[j]) {
5219
+ match = false;
5220
+ break;
5221
+ }
5222
+ }
5223
+ if (match) {
5224
+ let endIdx = -1;
5225
+ for (let k = i + 8; k <= pBuf.length - 8; k++) {
5226
+ let endMatch = true;
5227
+ for (let j = 0; j < 8; j++) {
5228
+ if (pBuf[k + j] !== iendSig[j]) {
5229
+ endMatch = false;
5230
+ break;
5231
+ }
5232
+ }
5233
+ if (endMatch) {
5234
+ endIdx = k + 8;
5235
+ break;
5236
+ }
5237
+ }
5238
+ if (endIdx !== -1) {
5239
+ const pngBytes = pBuf.subarray(i, endIdx);
5240
+ const blob = new Blob([pngBytes], { type: "image/png" });
5241
+ const url = URL.createObjectURL(blob);
5242
+ urls.push(url);
5243
+ createdUrls.push(url);
5244
+ i = endIdx;
5245
+ }
5246
+ }
5247
+ }
5248
+ for (let i = 0; i <= pBuf.length - 3; i++) {
5249
+ if (pBuf[i] === 255 && pBuf[i + 1] === 216 && pBuf[i + 2] === 255) {
5250
+ let endIdx = -1;
5251
+ for (let k = i + 3; k < pBuf.length - 1; k++) {
5252
+ if (pBuf[k] === 255 && pBuf[k + 1] === 217) {
5253
+ endIdx = k + 2;
5254
+ break;
5255
+ }
5256
+ }
5257
+ if (endIdx !== -1) {
5258
+ const jpgBytes = pBuf.subarray(i, endIdx);
5259
+ const blob = new Blob([jpgBytes], { type: "image/jpeg" });
5260
+ const url = URL.createObjectURL(blob);
5261
+ urls.push(url);
5262
+ createdUrls.push(url);
5263
+ i = endIdx;
5264
+ }
5265
+ }
5266
+ }
5267
+ } catch (err) {
5268
+ console.warn("[PptPlugin] Error extracting pictures:", err);
5269
+ }
5270
+ return urls;
5271
+ }
4567
5272
  /**
4568
5273
  * Traverse PowerPoint binary stream records ([MS-PPT]) and extract text chunks per slide
4569
5274
  */
4570
- extractSlides(stream) {
5275
+ extractSlides(stream, pictures) {
4571
5276
  const view = new DataView(stream.buffer, stream.byteOffset, stream.byteLength);
4572
5277
  const len = stream.length;
4573
5278
  let offset = 0;
4574
5279
  const slides = [];
4575
- let currentSlideTexts = [];
5280
+ let picIdx = 0;
4576
5281
  while (offset + 8 <= len) {
4577
5282
  const recVerInst = view.getUint16(offset, true);
4578
5283
  const recType = view.getUint16(offset + 2, true);
4579
5284
  const recLen = view.getUint32(offset + 4, true);
5285
+ const isContainer = (recVerInst & 15) === 15;
4580
5286
  if (recType === 1006) {
4581
- if (currentSlideTexts.length > 0) {
4582
- const title = currentSlideTexts[0] || "Slide";
4583
- const texts = currentSlideTexts.slice(1);
4584
- slides.push({ title, texts });
4585
- currentSlideTexts = [];
5287
+ const slideEnd = Math.min(len, offset + 8 + recLen);
5288
+ const rawTexts = [];
5289
+ let hasOle = false;
5290
+ let sOff = offset + 8;
5291
+ while (sOff + 8 <= slideEnd) {
5292
+ const cVerInst = view.getUint16(sOff, true);
5293
+ const cType = view.getUint16(sOff + 2, true);
5294
+ const cLen = view.getUint32(sOff + 4, true);
5295
+ const cIsContainer = (cVerInst & 15) === 15;
5296
+ if ((cType === 4008 || cType === 3998) && cLen > 0 && sOff + 8 + cLen <= slideEnd) {
5297
+ const bytes = stream.subarray(sOff + 8, sOff + 8 + cLen);
5298
+ const txt = new TextDecoder("latin1").decode(bytes).trim();
5299
+ if (txt && !/^[\x00-\x1F]+$/.test(txt) && !txt.includes("[Content_Types]") && !txt.includes("_rels/")) {
5300
+ rawTexts.push(txt);
5301
+ }
5302
+ } else if (cType === 3999 && cLen > 0 && sOff + 8 + cLen <= slideEnd) {
5303
+ const bytes = stream.subarray(sOff + 8, sOff + 8 + cLen);
5304
+ const txt = new TextDecoder("utf-16le").decode(bytes).trim();
5305
+ if (txt && !/^[\x00-\x1F]+$/.test(txt) && !txt.includes("[Content_Types]") && !txt.includes("_rels/")) {
5306
+ rawTexts.push(txt);
5307
+ }
5308
+ } else if (cType === 3009 || cType === 3011) {
5309
+ hasOle = true;
5310
+ }
5311
+ if (cIsContainer) sOff += 8;
5312
+ else sOff += 8 + cLen;
4586
5313
  }
4587
- offset += 8;
4588
- continue;
4589
- }
4590
- if (recType === 3998 && recLen > 0 && offset + 8 + recLen <= len) {
4591
- const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
4592
- const text = new TextDecoder("latin1").decode(bytes).trim();
4593
- if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
4594
- currentSlideTexts.push(text);
5314
+ let title = "";
5315
+ let subtitle = "";
5316
+ const paragraphs = [];
5317
+ const tableColumns = [];
5318
+ for (const t of rawTexts) {
5319
+ if (!title && t.length < 60 && !t.includes("\n")) {
5320
+ title = t;
5321
+ } else if (t.startsWith("Column ") || title === "Table" && t.startsWith("Column")) {
5322
+ tableColumns.push(t);
5323
+ } else if (t.length < 35 && (t.includes("#") || t.toUpperCase() === t) && !subtitle) {
5324
+ subtitle = t;
5325
+ } else {
5326
+ paragraphs.push(t);
5327
+ }
4595
5328
  }
4596
- }
4597
- if (recType === 3999 && recLen > 0 && offset + 8 + recLen <= len) {
4598
- const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
4599
- const text = new TextDecoder("utf-16le").decode(bytes).trim();
4600
- if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
4601
- currentSlideTexts.push(text);
5329
+ let pictureUrl = null;
5330
+ if ((hasOle || rawTexts.some((t) => t.toLowerCase().includes("chart") || t.toLowerCase().includes("figure"))) && picIdx < pictures.length) {
5331
+ pictureUrl = pictures[picIdx++];
4602
5332
  }
5333
+ slides.push({
5334
+ slideIndex: slides.length + 1,
5335
+ title: title || `Slide ${slides.length + 1}`,
5336
+ subtitle,
5337
+ paragraphs,
5338
+ tableColumns,
5339
+ pictureUrl,
5340
+ hasOle
5341
+ });
4603
5342
  }
4604
- const isContainer = (recVerInst & 15) === 15;
4605
- if (isContainer) {
4606
- offset += 8;
4607
- } else {
4608
- offset += 8 + recLen;
4609
- }
4610
- }
4611
- if (currentSlideTexts.length > 0) {
4612
- const title = currentSlideTexts[0] || "Slide";
4613
- const texts = currentSlideTexts.slice(1);
4614
- slides.push({ title, texts });
5343
+ if (isContainer) offset += 8;
5344
+ else offset += 8 + recLen;
4615
5345
  }
4616
5346
  if (slides.length === 0) {
4617
5347
  const rawText = new TextDecoder("latin1", { fatal: false }).decode(stream);
4618
- const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{4,}/g) || [];
4619
- const filtered = matches.map((m) => m.trim()).filter((m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times"));
4620
- if (filtered.length > 0) {
4621
- const chunkSize = 4;
4622
- for (let i = 0; i < filtered.length; i += chunkSize) {
4623
- const chunk = filtered.slice(i, i + chunkSize);
5348
+ const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{5,}/g) || [];
5349
+ const clean = matches.map((m) => m.trim()).filter(
5350
+ (m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times") && !m.includes("[Content_Types]") && !m.includes("_rels/") && !m.includes("xml")
5351
+ );
5352
+ if (clean.length > 0) {
5353
+ const chunkSize = 3;
5354
+ for (let i = 0; i < clean.length; i += chunkSize) {
5355
+ const chunk = clean.slice(i, i + chunkSize);
4624
5356
  slides.push({
5357
+ slideIndex: slides.length + 1,
4625
5358
  title: chunk[0] || `Slide ${Math.floor(i / chunkSize) + 1}`,
4626
- texts: chunk.slice(1)
5359
+ subtitle: chunk.length > 2 ? chunk[1] : void 0,
5360
+ paragraphs: chunk.length > 2 ? chunk.slice(2) : chunk.slice(1),
5361
+ tableColumns: [],
5362
+ pictureUrl: pictures[slides.length] || null
4627
5363
  });
4628
5364
  }
4629
5365
  }