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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/vue.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { defineComponent, ref, onMounted, watch, onBeforeUnmount, h } from 'vue';
2
2
  import DOMPurify2 from 'dompurify';
3
+ import * as pdfjsLib from 'pdfjs-dist';
3
4
  import * as docx from 'docx-preview';
4
5
  import { unzipSync, strFromU8, unzip } from 'fflate';
5
6
  import * as XLSX from 'xlsx';
@@ -10,7 +11,7 @@ import * as THREE from 'three';
10
11
  import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
11
12
  import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
12
13
  import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
13
- import { RTFJS } from 'rtf.js';
14
+ import * as RTFJS from 'rtf.js/dist/RTFJS.bundle.js';
14
15
 
15
16
  // src/vue.ts
16
17
  var EventEmitter = class {
@@ -266,6 +267,39 @@ function detectOoxmlType(buffer) {
266
267
  }
267
268
  return "application/zip";
268
269
  }
270
+ function detectCfbfType(buffer) {
271
+ const bytes = new Uint8Array(buffer);
272
+ const hasUtf16le = (str) => {
273
+ const target = new Uint8Array(str.length * 2);
274
+ for (let i = 0; i < str.length; i++) {
275
+ target[i * 2] = str.charCodeAt(i);
276
+ target[i * 2 + 1] = 0;
277
+ }
278
+ const targetLen = target.length;
279
+ const max = bytes.length - targetLen;
280
+ for (let i = 0; i <= max; i++) {
281
+ let match = true;
282
+ for (let j = 0; j < targetLen; j++) {
283
+ if (bytes[i + j] !== target[j]) {
284
+ match = false;
285
+ break;
286
+ }
287
+ }
288
+ if (match) return true;
289
+ }
290
+ return false;
291
+ };
292
+ if (hasUtf16le("PowerPoint Document")) {
293
+ return { mime: "application/vnd.ms-powerpoint", extension: ".ppt" };
294
+ }
295
+ if (hasUtf16le("WordDocument")) {
296
+ return { mime: "application/msword", extension: ".doc" };
297
+ }
298
+ if (hasUtf16le("Workbook") || hasUtf16le("Book")) {
299
+ return { mime: "application/vnd.ms-excel", extension: ".xls" };
300
+ }
301
+ return null;
302
+ }
269
303
  function extractExtension(nameOrUrl) {
270
304
  try {
271
305
  const url = new URL(nameOrUrl);
@@ -330,6 +364,18 @@ async function sourceToArrayBuffer(source, signal) {
330
364
  } else {
331
365
  metadata.mimeType = metadata.mimeType ?? magicMime;
332
366
  }
367
+ } else if (magicMime === "application/x-cfbf") {
368
+ if (metadata.extension) {
369
+ metadata.mimeType = mimeFromExtension(metadata.extension) ?? magicMime;
370
+ } else {
371
+ const cfbf = detectCfbfType(buffer);
372
+ if (cfbf) {
373
+ metadata.mimeType = cfbf.mime;
374
+ metadata.extension = cfbf.extension;
375
+ } else {
376
+ metadata.mimeType = magicMime;
377
+ }
378
+ }
333
379
  } else {
334
380
  metadata.mimeType = magicMime;
335
381
  }
@@ -434,11 +480,21 @@ var ToolbarController = class {
434
480
  el;
435
481
  toolbarEl;
436
482
  actions = [];
483
+ pageInputEl = null;
484
+ pageLabelEl = null;
437
485
  constructor(container) {
438
486
  this.el = container;
439
487
  this.toolbarEl = createElement("div", { className: "fp-toolbar" });
440
488
  this.el.appendChild(this.toolbarEl);
441
489
  }
490
+ setPage(page, max) {
491
+ if (this.pageInputEl) {
492
+ this.pageInputEl.value = page.toString();
493
+ }
494
+ if (max !== void 0 && this.pageLabelEl) {
495
+ this.pageLabelEl.textContent = ` / ${max}`;
496
+ }
497
+ }
442
498
  update(actions) {
443
499
  this.actions = actions;
444
500
  this.render();
@@ -481,6 +537,7 @@ var ToolbarController = class {
481
537
  if (action.type === "separator") {
482
538
  groupEl.appendChild(createElement("div", { className: "fp-toolbar-separator" }));
483
539
  } else if (action.type === "page-nav") {
540
+ const max = action.max ?? 1;
484
541
  const prevBtn = this.createButton(
485
542
  "prev",
486
543
  ICON_PAGE_PREV,
@@ -499,7 +556,6 @@ var ToolbarController = class {
499
556
  "Next Page",
500
557
  () => {
501
558
  const cur = parseInt(input.value, 10) || 1;
502
- const max = action.max ?? 1;
503
559
  if (cur < max) {
504
560
  input.value = (cur + 1).toString();
505
561
  action.execute("next", cur + 1);
@@ -511,15 +567,18 @@ var ToolbarController = class {
511
567
  type: "number",
512
568
  value: (action.value ?? 1).toString(),
513
569
  min: "1",
514
- max: (action.max ?? 1).toString()
570
+ max: max.toString()
515
571
  });
516
572
  input.addEventListener("change", () => {
517
- const val = parseInt(input.value, 10);
518
- if (!isNaN(val)) {
519
- action.execute("go", val);
520
- }
573
+ let val = parseInt(input.value, 10);
574
+ if (isNaN(val)) val = 1;
575
+ val = Math.max(1, Math.min(max, val));
576
+ input.value = val.toString();
577
+ action.execute("go", val);
521
578
  });
522
- const label = createElement("span", { className: "fp-toolbar-label" }, ` / ${action.max ?? 1}`);
579
+ const label = createElement("span", { className: "fp-toolbar-label" }, ` / ${max}`);
580
+ this.pageInputEl = input;
581
+ this.pageLabelEl = label;
523
582
  groupEl.appendChild(prevBtn);
524
583
  groupEl.appendChild(input);
525
584
  groupEl.appendChild(label);
@@ -697,7 +756,13 @@ var FilePreviewViewer = class {
697
756
  buffer,
698
757
  options,
699
758
  signal,
700
- emit: (event, payload) => this.eventEmitter.emit(event, payload)
759
+ emit: (event, payload) => {
760
+ if (event === "page-change" && payload && typeof payload.page === "number") {
761
+ const total = payload.total ?? payload.totalPages;
762
+ this.toolbar?.setPage(payload.page, total);
763
+ }
764
+ this.eventEmitter.emit(event, payload);
765
+ }
701
766
  });
702
767
  this.activeInstance = instance;
703
768
  this.hideLoading();
@@ -731,6 +796,7 @@ var FilePreviewViewer = class {
731
796
  if (thumbnails && thumbnails.length > 0 && this.thumbnailPanel) {
732
797
  this.thumbnailPanel.update(thumbnails, (index) => {
733
798
  instance.goToPage?.(index + 1);
799
+ this.toolbar?.setPage(index + 1);
734
800
  });
735
801
  if (options.showThumbnails) {
736
802
  this.thumbnailPanel.show();
@@ -856,9 +922,13 @@ var FilePreviewViewer = class {
856
922
  if (e.key === "ArrowRight" || e.key === "PageDown") {
857
923
  const cur = this.activeInstance.getCurrentPage?.() ?? 1;
858
924
  this.activeInstance.goToPage?.(cur + 1);
925
+ const nextCur = this.activeInstance.getCurrentPage?.() ?? cur + 1;
926
+ this.toolbar?.setPage(nextCur);
859
927
  } else if (e.key === "ArrowLeft" || e.key === "PageUp") {
860
928
  const cur = this.activeInstance.getCurrentPage?.() ?? 1;
861
929
  this.activeInstance.goToPage?.(Math.max(1, cur - 1));
930
+ const prevCur = this.activeInstance.getCurrentPage?.() ?? Math.max(1, cur - 1);
931
+ this.toolbar?.setPage(prevCur);
862
932
  } else if (e.key === "+" || e.key === "=") {
863
933
  this.activeInstance.zoomIn?.();
864
934
  } else if (e.key === "-" || e.key === "_") {
@@ -1124,30 +1194,62 @@ var CfbfReader = class {
1124
1194
  return result.subarray(0, targetSize);
1125
1195
  }
1126
1196
  };
1127
-
1128
- // ../plugins/pdf/dist/index.js
1197
+ if (typeof window !== "undefined" && pdfjsLib.GlobalWorkerOptions) {
1198
+ if (!pdfjsLib.GlobalWorkerOptions.workerSrc) {
1199
+ pdfjsLib.GlobalWorkerOptions.workerSrc = `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/build/pdf.worker.min.mjs`;
1200
+ }
1201
+ }
1129
1202
  var PdfPlugin = class {
1130
1203
  id = "pdf";
1131
- name = "PDF Preview";
1204
+ name = "PDF Document Preview";
1132
1205
  extensions = [".pdf"];
1133
1206
  mimeTypes = ["application/pdf"];
1134
1207
  weight = 100;
1135
1208
  supports(file) {
1136
1209
  const ext = file.metadata.extension?.toLowerCase();
1137
1210
  const mime = file.metadata.mimeType?.toLowerCase();
1138
- return ext === ".pdf" || mime === "application/pdf";
1211
+ if (ext) return this.extensions.includes(ext);
1212
+ return this.mimeTypes.includes(mime || "");
1139
1213
  }
1140
1214
  getToolbarActions(instance) {
1215
+ const totalPages = instance.getPageCount?.() ?? 1;
1216
+ const curPage = instance.getCurrentPage?.() ?? 1;
1141
1217
  return [
1218
+ {
1219
+ id: "thumbnails",
1220
+ icon: "thumbnails",
1221
+ label: "Page Thumbnails",
1222
+ type: "button",
1223
+ group: "navigation",
1224
+ execute: () => instance.toggleThumbnails?.()
1225
+ },
1226
+ {
1227
+ id: "page-nav",
1228
+ icon: "",
1229
+ label: "Page Navigation",
1230
+ type: "page-nav",
1231
+ group: "navigation",
1232
+ value: curPage,
1233
+ max: totalPages,
1234
+ execute: (action, page) => {
1235
+ const cur = instance.getCurrentPage?.() ?? 1;
1236
+ const max = instance.getPageCount?.() ?? 1;
1237
+ if (action === "prev") {
1238
+ if (cur > 1) instance.goToPage?.(cur - 1);
1239
+ } else if (action === "next") {
1240
+ if (cur < max) instance.goToPage?.(cur + 1);
1241
+ } else if (typeof page === "number") {
1242
+ instance.goToPage?.(page);
1243
+ }
1244
+ }
1245
+ },
1142
1246
  {
1143
1247
  id: "zoom-out",
1144
1248
  icon: "zoom-out",
1145
1249
  label: "Zoom Out",
1146
1250
  type: "button",
1147
1251
  group: "zoom",
1148
- execute: () => {
1149
- instance.zoomOut?.();
1150
- }
1252
+ execute: () => instance.zoomOut?.()
1151
1253
  },
1152
1254
  {
1153
1255
  id: "zoom-in",
@@ -1155,9 +1257,7 @@ var PdfPlugin = class {
1155
1257
  label: "Zoom In",
1156
1258
  type: "button",
1157
1259
  group: "zoom",
1158
- execute: () => {
1159
- instance.zoomIn?.();
1160
- }
1260
+ execute: () => instance.zoomIn?.()
1161
1261
  },
1162
1262
  {
1163
1263
  id: "fit-page",
@@ -1165,39 +1265,23 @@ var PdfPlugin = class {
1165
1265
  label: "Fit to Page",
1166
1266
  type: "button",
1167
1267
  group: "zoom",
1168
- execute: () => {
1169
- instance.fitToPage?.();
1170
- }
1268
+ execute: () => instance.fitToPage?.()
1171
1269
  },
1172
1270
  {
1173
1271
  id: "rotate-cw",
1174
1272
  icon: "rotate-cw",
1175
- label: "Rotate",
1273
+ label: "Rotate Clockwise",
1176
1274
  type: "button",
1177
1275
  group: "view",
1178
- execute: () => {
1179
- instance.rotateCW?.();
1180
- }
1181
- },
1182
- {
1183
- id: "page-nav",
1184
- icon: "page-nav",
1185
- label: "Page Navigation",
1186
- type: "page-nav",
1187
- group: "navigation",
1188
- execute: (page) => {
1189
- if (typeof page === "number") instance.goToPage?.(page);
1190
- }
1276
+ execute: () => instance.rotateCW?.()
1191
1277
  },
1192
1278
  {
1193
1279
  id: "download",
1194
1280
  icon: "download",
1195
- label: "Download",
1281
+ label: "Download PDF",
1196
1282
  type: "button",
1197
1283
  group: "actions",
1198
- execute: () => {
1199
- instance.download?.();
1200
- }
1284
+ execute: () => instance.download?.()
1201
1285
  },
1202
1286
  {
1203
1287
  id: "print",
@@ -1205,99 +1289,213 @@ var PdfPlugin = class {
1205
1289
  label: "Print",
1206
1290
  type: "button",
1207
1291
  group: "actions",
1208
- execute: () => {
1209
- instance.print?.();
1210
- }
1292
+ execute: () => instance.print?.()
1211
1293
  }
1212
1294
  ];
1213
1295
  }
1214
1296
  async render(ctx) {
1215
- const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1216
- const url = URL.createObjectURL(blob);
1217
- const wrapper = document.createElement("div");
1218
- wrapper.style.width = "100%";
1219
- wrapper.style.height = "100%";
1220
- wrapper.style.overflow = "hidden";
1221
- wrapper.style.display = "flex";
1222
- wrapper.style.justifyContent = "center";
1223
- wrapper.style.alignItems = "center";
1224
- const iframe = document.createElement("iframe");
1225
- iframe.src = url;
1226
- iframe.style.width = "100%";
1227
- iframe.style.height = "100%";
1228
- iframe.style.border = "none";
1229
- wrapper.appendChild(iframe);
1230
- ctx.container.appendChild(wrapper);
1297
+ const container = document.createElement("div");
1298
+ container.className = "fp-pdf-container";
1299
+ container.style.width = "100%";
1300
+ container.style.height = "100%";
1301
+ container.style.overflow = "auto";
1302
+ container.style.display = "flex";
1303
+ container.style.flexDirection = "column";
1304
+ container.style.alignItems = "center";
1305
+ container.style.padding = "24px 16px";
1306
+ container.style.backgroundColor = "#0f172a";
1307
+ container.style.boxSizing = "border-box";
1308
+ container.style.position = "relative";
1309
+ const pageCard = document.createElement("div");
1310
+ pageCard.className = "fp-pdf-page-card";
1311
+ pageCard.style.boxShadow = "0 10px 35px rgba(0, 0, 0, 0.5)";
1312
+ pageCard.style.backgroundColor = "#ffffff";
1313
+ pageCard.style.borderRadius = "4px";
1314
+ pageCard.style.overflow = "hidden";
1315
+ pageCard.style.lineHeight = "0";
1316
+ pageCard.style.transition = "transform 0.15s ease";
1317
+ pageCard.style.position = "relative";
1318
+ const canvas = document.createElement("canvas");
1319
+ pageCard.appendChild(canvas);
1320
+ container.appendChild(pageCard);
1321
+ const indicator = document.createElement("div");
1322
+ indicator.className = "fp-pdf-page-indicator";
1323
+ indicator.style.position = "sticky";
1324
+ indicator.style.bottom = "16px";
1325
+ indicator.style.marginTop = "16px";
1326
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
1327
+ indicator.style.backdropFilter = "blur(8px)";
1328
+ indicator.style.color = "#f8fafc";
1329
+ indicator.style.fontSize = "12px";
1330
+ indicator.style.fontWeight = "600";
1331
+ indicator.style.padding = "5px 14px";
1332
+ indicator.style.borderRadius = "20px";
1333
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
1334
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
1335
+ indicator.style.zIndex = "10";
1336
+ indicator.style.userSelect = "none";
1337
+ indicator.style.pointerEvents = "none";
1338
+ container.appendChild(indicator);
1339
+ ctx.container.appendChild(container);
1340
+ const loadingTask = pdfjsLib.getDocument({
1341
+ data: new Uint8Array(ctx.buffer),
1342
+ cMapUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/cmaps/`,
1343
+ cMapPacked: true,
1344
+ standardFontDataUrl: `https://unpkg.com/pdfjs-dist@${pdfjsLib.version || "4.10.38"}/standard_fonts/`
1345
+ });
1346
+ const pdfDoc = await loadingTask.promise;
1347
+ const totalPages = Math.max(1, pdfDoc.numPages);
1231
1348
  let currentPage = 1;
1232
- let currentZoom = 1;
1349
+ let zoomScale = 1;
1233
1350
  let rotation = 0;
1351
+ let currentRenderTask = null;
1352
+ const renderPage = async (pageNum) => {
1353
+ if (currentRenderTask) {
1354
+ try {
1355
+ currentRenderTask.cancel();
1356
+ } catch {
1357
+ }
1358
+ currentRenderTask = null;
1359
+ }
1360
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
1361
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
1362
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
1363
+ const page = await pdfDoc.getPage(currentPage);
1364
+ const containerWidth = container.clientWidth || 900;
1365
+ const unscaledVp = page.getViewport({ scale: 1, rotation });
1366
+ const baseScale = Math.min((containerWidth - 64) / unscaledVp.width, 1.6);
1367
+ const effectiveScale = (baseScale > 0 ? baseScale : 1) * zoomScale;
1368
+ const pixelRatio = window.devicePixelRatio || 1;
1369
+ const viewport = page.getViewport({ scale: effectiveScale, rotation });
1370
+ canvas.width = Math.floor(viewport.width * pixelRatio);
1371
+ canvas.height = Math.floor(viewport.height * pixelRatio);
1372
+ canvas.style.width = `${Math.floor(viewport.width)}px`;
1373
+ canvas.style.height = `${Math.floor(viewport.height)}px`;
1374
+ const canvasCtx = canvas.getContext("2d");
1375
+ if (!canvasCtx) return;
1376
+ canvasCtx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
1377
+ currentRenderTask = page.render({
1378
+ canvasContext: canvasCtx,
1379
+ viewport
1380
+ });
1381
+ try {
1382
+ await currentRenderTask.promise;
1383
+ } catch (err) {
1384
+ if (err?.name !== "RenderingCancelledException") {
1385
+ console.warn("[PdfPlugin] Page render warning:", err);
1386
+ }
1387
+ } finally {
1388
+ currentRenderTask = null;
1389
+ }
1390
+ };
1391
+ await renderPage(1);
1234
1392
  const cleanup = () => {
1235
- URL.revokeObjectURL(url);
1236
- wrapper.remove();
1393
+ if (currentRenderTask) {
1394
+ try {
1395
+ currentRenderTask.cancel();
1396
+ } catch {
1397
+ }
1398
+ }
1399
+ try {
1400
+ pdfDoc.destroy();
1401
+ } catch {
1402
+ }
1403
+ container.remove();
1237
1404
  ctx.container.innerHTML = "";
1238
1405
  };
1239
1406
  ctx.signal.addEventListener("abort", cleanup);
1240
- return {
1407
+ const instance = {
1241
1408
  destroy: cleanup,
1242
1409
  zoomIn: () => {
1243
- currentZoom += 0.1;
1244
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1410
+ zoomScale = Math.min(3.5, zoomScale + 0.2);
1411
+ renderPage(currentPage);
1245
1412
  },
1246
1413
  zoomOut: () => {
1247
- currentZoom = Math.max(0.2, currentZoom - 0.1);
1248
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1414
+ zoomScale = Math.max(0.3, zoomScale - 0.2);
1415
+ renderPage(currentPage);
1249
1416
  },
1250
- getZoom: () => currentZoom,
1417
+ getZoom: () => zoomScale,
1251
1418
  setZoom: (level) => {
1252
- currentZoom = level;
1253
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1419
+ zoomScale = Math.max(0.3, Math.min(3.5, level));
1420
+ renderPage(currentPage);
1254
1421
  },
1255
1422
  fitToPage: () => {
1256
- currentZoom = 1;
1257
- iframe.style.transform = `scale(1) rotate(${rotation}deg)`;
1423
+ zoomScale = 1;
1424
+ renderPage(currentPage);
1258
1425
  },
1259
1426
  rotateCW: () => {
1260
1427
  rotation = (rotation + 90) % 360;
1261
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1428
+ renderPage(currentPage);
1262
1429
  },
1263
1430
  rotateCCW: () => {
1264
1431
  rotation = (rotation - 90 + 360) % 360;
1265
- iframe.style.transform = `scale(${currentZoom}) rotate(${rotation}deg)`;
1432
+ renderPage(currentPage);
1266
1433
  },
1267
1434
  getRotation: () => rotation,
1435
+ getPageCount: () => totalPages,
1436
+ getCurrentPage: () => currentPage,
1268
1437
  goToPage: (page) => {
1269
- currentPage = page;
1270
- iframe.src = `${url}#page=${page}`;
1438
+ renderPage(page);
1271
1439
  },
1272
- getCurrentPage: () => currentPage,
1273
1440
  download: () => {
1441
+ const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1442
+ const url = URL.createObjectURL(blob);
1274
1443
  const a = document.createElement("a");
1275
1444
  a.href = url;
1276
1445
  a.download = ctx.metadata.name || "document.pdf";
1277
1446
  a.click();
1447
+ URL.revokeObjectURL(url);
1278
1448
  },
1279
1449
  print: () => {
1280
- iframe.contentWindow?.print();
1450
+ const blob = new Blob([ctx.buffer], { type: "application/pdf" });
1451
+ const url = URL.createObjectURL(blob);
1452
+ const hiddenIframe = document.createElement("iframe");
1453
+ hiddenIframe.style.position = "fixed";
1454
+ hiddenIframe.style.right = "0";
1455
+ hiddenIframe.style.bottom = "0";
1456
+ hiddenIframe.style.width = "0";
1457
+ hiddenIframe.style.height = "0";
1458
+ hiddenIframe.style.border = "0";
1459
+ document.body.appendChild(hiddenIframe);
1460
+ hiddenIframe.src = url;
1461
+ hiddenIframe.onload = () => {
1462
+ setTimeout(() => {
1463
+ hiddenIframe.contentWindow?.print();
1464
+ setTimeout(() => {
1465
+ hiddenIframe.remove();
1466
+ URL.revokeObjectURL(url);
1467
+ }, 1e3);
1468
+ }, 300);
1469
+ };
1281
1470
  },
1282
1471
  getThumbnails: async () => {
1283
- return [
1284
- {
1285
- index: 1,
1286
- label: "Page 1",
1287
- render: async (canvas) => {
1288
- const context = canvas.getContext("2d");
1289
- if (context) {
1290
- context.fillStyle = "#fff";
1291
- context.fillRect(0, 0, canvas.width, canvas.height);
1292
- context.fillStyle = "#333";
1293
- context.font = "12px sans-serif";
1294
- context.fillText("PDF Preview", 10, 20);
1472
+ const thumbnails = [];
1473
+ const count = Math.min(totalPages, 50);
1474
+ for (let i = 1; i <= count; i++) {
1475
+ thumbnails.push({
1476
+ index: i,
1477
+ label: `Page ${i}`,
1478
+ render: async (thumbCanvas) => {
1479
+ try {
1480
+ const p = await pdfDoc.getPage(i);
1481
+ const baseVp = p.getViewport({ scale: 1 });
1482
+ const thumbScale = (thumbCanvas.width || 120) / baseVp.width;
1483
+ const thumbVp = p.getViewport({ scale: thumbScale });
1484
+ thumbCanvas.height = Math.floor(thumbVp.height);
1485
+ const tCtx = thumbCanvas.getContext("2d");
1486
+ if (tCtx) {
1487
+ await p.render({ canvasContext: tCtx, viewport: thumbVp }).promise;
1488
+ }
1489
+ } catch (e) {
1490
+ console.warn(`[PdfPlugin] Error generating thumbnail for page ${i}:`, e);
1295
1491
  }
1296
1492
  }
1297
- }
1298
- ];
1493
+ });
1494
+ }
1495
+ return thumbnails;
1299
1496
  }
1300
1497
  };
1498
+ return instance;
1301
1499
  }
1302
1500
  };
1303
1501
  function pdfPlugin() {
@@ -1618,10 +1816,37 @@ var DocxPlugin = class {
1618
1816
  supports(file) {
1619
1817
  const ext = file.metadata.extension?.toLowerCase();
1620
1818
  const mime = file.metadata.mimeType?.toLowerCase();
1621
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
1819
+ if (ext) {
1820
+ return this.extensions.includes(ext);
1821
+ }
1822
+ return this.mimeTypes.includes(mime || "");
1622
1823
  }
1623
1824
  getToolbarActions(instance) {
1624
- return [
1825
+ const totalPages = instance.getPageCount?.() ?? 1;
1826
+ const actions = [];
1827
+ if (totalPages > 1) {
1828
+ actions.push({
1829
+ id: "page-nav",
1830
+ icon: "",
1831
+ label: "Page Navigation",
1832
+ type: "page-nav",
1833
+ group: "navigation",
1834
+ value: instance.getCurrentPage?.() ?? 1,
1835
+ max: totalPages,
1836
+ execute: (action, page) => {
1837
+ const cur = instance.getCurrentPage?.() ?? 1;
1838
+ const max = instance.getPageCount?.() ?? 1;
1839
+ if (action === "prev") {
1840
+ if (cur > 1) instance.goToPage?.(cur - 1);
1841
+ } else if (action === "next") {
1842
+ if (cur < max) instance.goToPage?.(cur + 1);
1843
+ } else if (typeof page === "number") {
1844
+ instance.goToPage?.(page);
1845
+ }
1846
+ }
1847
+ });
1848
+ }
1849
+ actions.push(
1625
1850
  {
1626
1851
  id: "zoom-out",
1627
1852
  icon: "zoom-out",
@@ -1662,7 +1887,8 @@ var DocxPlugin = class {
1662
1887
  group: "actions",
1663
1888
  execute: () => instance.print?.()
1664
1889
  }
1665
- ];
1890
+ );
1891
+ return actions;
1666
1892
  }
1667
1893
  async render(ctx) {
1668
1894
  const wrapper = document.createElement("div");
@@ -1738,7 +1964,51 @@ var DocxPlugin = class {
1738
1964
  }
1739
1965
  }
1740
1966
  }
1967
+ const sections = wrapper.querySelectorAll("section.docx");
1968
+ const cards = wrapper.querySelectorAll(".fp-docx-page-card");
1969
+ const pageElements = sections.length > 0 ? sections : cards;
1970
+ const totalPages = Math.max(1, pageElements.length);
1971
+ let currentPage = 1;
1972
+ let indicator = null;
1973
+ if (totalPages > 1) {
1974
+ indicator = document.createElement("div");
1975
+ indicator.className = "fp-docx-page-indicator";
1976
+ indicator.style.position = "sticky";
1977
+ indicator.style.bottom = "16px";
1978
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
1979
+ indicator.style.backdropFilter = "blur(8px)";
1980
+ indicator.style.color = "#f8fafc";
1981
+ indicator.style.fontSize = "12px";
1982
+ indicator.style.fontWeight = "600";
1983
+ indicator.style.padding = "5px 14px";
1984
+ indicator.style.borderRadius = "20px";
1985
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
1986
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
1987
+ indicator.style.zIndex = "10";
1988
+ indicator.style.userSelect = "none";
1989
+ indicator.style.pointerEvents = "none";
1990
+ indicator.style.textAlign = "center";
1991
+ indicator.style.width = "fit-content";
1992
+ indicator.style.margin = "16px auto 0";
1993
+ ctx.container.appendChild(indicator);
1994
+ }
1995
+ const showPage = (pageNum) => {
1996
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
1997
+ if (pageElements.length > 1) {
1998
+ pageElements.forEach((sec, idx) => {
1999
+ sec.style.display = idx + 1 === currentPage ? "block" : "none";
2000
+ });
2001
+ }
2002
+ if (indicator) {
2003
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
2004
+ }
2005
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
2006
+ };
2007
+ if (totalPages > 1) {
2008
+ showPage(1);
2009
+ }
1741
2010
  const cleanup = () => {
2011
+ indicator?.remove();
1742
2012
  for (const url of createdBlobUrls) {
1743
2013
  URL.revokeObjectURL(url);
1744
2014
  }
@@ -1749,6 +2019,11 @@ var DocxPlugin = class {
1749
2019
  ctx.signal.addEventListener("abort", cleanup);
1750
2020
  return {
1751
2021
  destroy: cleanup,
2022
+ getPageCount: () => totalPages,
2023
+ getCurrentPage: () => currentPage,
2024
+ goToPage: (page) => {
2025
+ showPage(page);
2026
+ },
1752
2027
  zoomIn: () => {
1753
2028
  scale += 0.1;
1754
2029
  wrapper.style.transform = `scale(${scale})`;
@@ -2007,10 +2282,37 @@ var ExcelPlugin = class {
2007
2282
  supports(file) {
2008
2283
  const ext = file.metadata.extension?.toLowerCase();
2009
2284
  const mime = file.metadata.mimeType?.toLowerCase();
2010
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
2285
+ if (ext) {
2286
+ return this.extensions.includes(ext);
2287
+ }
2288
+ return this.mimeTypes.includes(mime || "");
2011
2289
  }
2012
2290
  getToolbarActions(instance) {
2013
- return [
2291
+ const totalSheets = instance.getPageCount?.() ?? 1;
2292
+ const actions = [];
2293
+ if (totalSheets > 1) {
2294
+ actions.push({
2295
+ id: "page-nav",
2296
+ icon: "",
2297
+ label: "Sheet Navigation",
2298
+ type: "page-nav",
2299
+ group: "navigation",
2300
+ value: instance.getCurrentPage?.() ?? 1,
2301
+ max: totalSheets,
2302
+ execute: (action, sheet) => {
2303
+ const cur = instance.getCurrentPage?.() ?? 1;
2304
+ const max = instance.getPageCount?.() ?? 1;
2305
+ if (action === "prev") {
2306
+ if (cur > 1) instance.goToPage?.(cur - 1);
2307
+ } else if (action === "next") {
2308
+ if (cur < max) instance.goToPage?.(cur + 1);
2309
+ } else if (typeof sheet === "number") {
2310
+ instance.goToPage?.(sheet);
2311
+ }
2312
+ }
2313
+ });
2314
+ }
2315
+ actions.push(
2014
2316
  {
2015
2317
  id: "zoom-out",
2016
2318
  icon: "zoom-out",
@@ -2031,16 +2333,6 @@ var ExcelPlugin = class {
2031
2333
  instance.zoomIn?.();
2032
2334
  }
2033
2335
  },
2034
- {
2035
- id: "page-nav",
2036
- icon: "page-nav",
2037
- label: "Sheet Navigation",
2038
- type: "page-nav",
2039
- group: "navigation",
2040
- execute: (sheet) => {
2041
- if (typeof sheet === "number") instance.goToPage?.(sheet);
2042
- }
2043
- },
2044
2336
  {
2045
2337
  id: "download",
2046
2338
  icon: "download",
@@ -2061,7 +2353,8 @@ var ExcelPlugin = class {
2061
2353
  instance.print?.();
2062
2354
  }
2063
2355
  }
2064
- ];
2356
+ );
2357
+ return actions;
2065
2358
  }
2066
2359
  async render(ctx) {
2067
2360
  const container = document.createElement("div");
@@ -2145,6 +2438,7 @@ var ExcelPlugin = class {
2145
2438
  b.style.fontWeight = "normal";
2146
2439
  }
2147
2440
  });
2441
+ ctx.emit("page-change", { page: currentSheetIndex, total: sheetNames.length });
2148
2442
  };
2149
2443
  if (sheetNames.length > 0) {
2150
2444
  sheetNames.forEach((name, idx) => {
@@ -2394,7 +2688,31 @@ var CodePlugin = class {
2394
2688
  return false;
2395
2689
  }
2396
2690
  getToolbarActions(instance) {
2397
- return [
2691
+ const totalPages = instance.getPageCount?.() ?? 1;
2692
+ const actions = [];
2693
+ if (totalPages > 1) {
2694
+ actions.push({
2695
+ id: "page-nav",
2696
+ icon: "",
2697
+ label: "Page Navigation",
2698
+ type: "page-nav",
2699
+ group: "navigation",
2700
+ value: instance.getCurrentPage?.() ?? 1,
2701
+ max: totalPages,
2702
+ execute: (action, page) => {
2703
+ const cur = instance.getCurrentPage?.() ?? 1;
2704
+ const max = instance.getPageCount?.() ?? 1;
2705
+ if (action === "prev") {
2706
+ if (cur > 1) instance.goToPage?.(cur - 1);
2707
+ } else if (action === "next") {
2708
+ if (cur < max) instance.goToPage?.(cur + 1);
2709
+ } else if (typeof page === "number") {
2710
+ instance.goToPage?.(page);
2711
+ }
2712
+ }
2713
+ });
2714
+ }
2715
+ actions.push(
2398
2716
  {
2399
2717
  id: "zoom-out",
2400
2718
  icon: "zoom-out",
@@ -2445,11 +2763,16 @@ var CodePlugin = class {
2445
2763
  instance.print?.();
2446
2764
  }
2447
2765
  }
2448
- ];
2766
+ );
2767
+ return actions;
2449
2768
  }
2450
2769
  async render(ctx) {
2451
2770
  const decoder = new TextDecoder("utf-8");
2452
- const text = decoder.decode(ctx.buffer);
2771
+ const fullText = decoder.decode(ctx.buffer);
2772
+ const pageSplitRegex = /(?:\f|\x0C|(?:\r?\n|^)\s*[-=_]{3,}\s*(?:PAGE|Page|page break|Page Break)[\s\d\w-]*[-=_]{3,}\s*(?:\r?\n|$))/i;
2773
+ const rawPages = fullText.split(pageSplitRegex).map((p) => p.trim()).filter((p) => p.length > 0);
2774
+ const totalPages = Math.max(1, rawPages.length);
2775
+ let currentPage = 1;
2453
2776
  const container = document.createElement("div");
2454
2777
  container.style.width = "100%";
2455
2778
  container.style.height = "100%";
@@ -2468,25 +2791,66 @@ var CodePlugin = class {
2468
2791
  pre.style.wordBreak = "break-all";
2469
2792
  const code = document.createElement("code");
2470
2793
  const ext = (ctx.metadata.extension || "").replace(".", "");
2471
- try {
2472
- if (ext && hljs.getLanguage(ext)) {
2473
- code.innerHTML = hljs.highlight(text, { language: ext }).value;
2474
- } else {
2475
- code.innerHTML = hljs.highlightAuto(text).value;
2794
+ const renderCodePage = (text) => {
2795
+ try {
2796
+ if (ext && hljs.getLanguage(ext)) {
2797
+ code.innerHTML = hljs.highlight(text, { language: ext }).value;
2798
+ } else {
2799
+ code.innerHTML = hljs.highlightAuto(text).value;
2800
+ }
2801
+ } catch {
2802
+ code.textContent = text;
2476
2803
  }
2477
- } catch {
2478
- code.textContent = text;
2479
- }
2804
+ };
2805
+ renderCodePage(rawPages[0] || fullText);
2480
2806
  pre.appendChild(code);
2481
2807
  container.appendChild(pre);
2808
+ let indicator = null;
2809
+ if (totalPages > 1) {
2810
+ indicator = document.createElement("div");
2811
+ indicator.className = "fp-code-page-indicator";
2812
+ indicator.style.position = "sticky";
2813
+ indicator.style.bottom = "16px";
2814
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
2815
+ indicator.style.backdropFilter = "blur(8px)";
2816
+ indicator.style.color = "#f8fafc";
2817
+ indicator.style.fontSize = "12px";
2818
+ indicator.style.fontWeight = "600";
2819
+ indicator.style.padding = "5px 14px";
2820
+ indicator.style.borderRadius = "20px";
2821
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
2822
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
2823
+ indicator.style.zIndex = "10";
2824
+ indicator.style.userSelect = "none";
2825
+ indicator.style.pointerEvents = "none";
2826
+ indicator.style.textAlign = "center";
2827
+ indicator.style.width = "fit-content";
2828
+ indicator.style.margin = "16px auto 0";
2829
+ container.appendChild(indicator);
2830
+ }
2831
+ const showPage = (pageNum) => {
2832
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
2833
+ renderCodePage(rawPages[currentPage - 1] || fullText);
2834
+ if (indicator) {
2835
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
2836
+ }
2837
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
2838
+ };
2839
+ if (totalPages > 1) {
2840
+ showPage(1);
2841
+ }
2482
2842
  ctx.container.appendChild(container);
2483
2843
  const cleanup = () => {
2844
+ indicator?.remove();
2484
2845
  container.remove();
2485
2846
  ctx.container.innerHTML = "";
2486
2847
  };
2487
2848
  ctx.signal.addEventListener("abort", cleanup);
2488
2849
  return {
2489
2850
  destroy: cleanup,
2851
+ getPageCount: () => totalPages,
2852
+ getCurrentPage: () => currentPage,
2853
+ goToPage: (page) => showPage(page),
2490
2854
  zoomIn: () => {
2491
2855
  fontSize = Math.min(32, fontSize + 2);
2492
2856
  pre.style.fontSize = `${fontSize}px`;
@@ -2514,7 +2878,7 @@ var CodePlugin = class {
2514
2878
  window.print();
2515
2879
  },
2516
2880
  copy: () => {
2517
- navigator.clipboard?.writeText(text);
2881
+ navigator.clipboard?.writeText(fullText);
2518
2882
  }
2519
2883
  };
2520
2884
  }
@@ -2949,7 +3313,10 @@ var PptxPlugin = class {
2949
3313
  supports(file) {
2950
3314
  const ext = file.metadata.extension?.toLowerCase();
2951
3315
  const mime = file.metadata.mimeType?.toLowerCase();
2952
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3316
+ if (ext) {
3317
+ return this.extensions.includes(ext);
3318
+ }
3319
+ return this.mimeTypes.includes(mime || "");
2953
3320
  }
2954
3321
  getToolbarActions(instance) {
2955
3322
  return [
@@ -3333,7 +3700,31 @@ var RtfPlugin = class {
3333
3700
  return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3334
3701
  }
3335
3702
  getToolbarActions(instance) {
3336
- return [
3703
+ const totalPages = instance.getPageCount?.() ?? 1;
3704
+ const actions = [];
3705
+ if (totalPages > 1) {
3706
+ actions.push({
3707
+ id: "page-nav",
3708
+ icon: "",
3709
+ label: "Page Navigation",
3710
+ type: "page-nav",
3711
+ group: "navigation",
3712
+ value: instance.getCurrentPage?.() ?? 1,
3713
+ max: totalPages,
3714
+ execute: (action, page) => {
3715
+ const cur = instance.getCurrentPage?.() ?? 1;
3716
+ const max = instance.getPageCount?.() ?? 1;
3717
+ if (action === "prev") {
3718
+ if (cur > 1) instance.goToPage?.(cur - 1);
3719
+ } else if (action === "next") {
3720
+ if (cur < max) instance.goToPage?.(cur + 1);
3721
+ } else if (typeof page === "number") {
3722
+ instance.goToPage?.(page);
3723
+ }
3724
+ }
3725
+ });
3726
+ }
3727
+ actions.push(
3337
3728
  {
3338
3729
  id: "zoom-out",
3339
3730
  icon: "zoom-out",
@@ -3374,7 +3765,8 @@ var RtfPlugin = class {
3374
3765
  group: "actions",
3375
3766
  execute: () => instance.print?.()
3376
3767
  }
3377
- ];
3768
+ );
3769
+ return actions;
3378
3770
  }
3379
3771
  async render(ctx) {
3380
3772
  const wrapper = document.createElement("div");
@@ -3393,25 +3785,82 @@ var RtfPlugin = class {
3393
3785
  ctx.container.style.backgroundColor = "#f1f5f9";
3394
3786
  ctx.container.appendChild(wrapper);
3395
3787
  let scale = 1;
3788
+ let pageElements = [];
3396
3789
  try {
3790
+ if (typeof RTFJS.loggingEnabled === "function") {
3791
+ RTFJS.loggingEnabled(false);
3792
+ }
3397
3793
  const doc = new RTFJS.Document(ctx.buffer, {});
3398
3794
  const htmlElements = await doc.render();
3399
- for (const el of htmlElements) {
3795
+ pageElements = htmlElements;
3796
+ for (let i = 0; i < htmlElements.length; i++) {
3797
+ const el = htmlElements[i];
3798
+ el.style.display = i === 0 ? "block" : "none";
3400
3799
  wrapper.appendChild(el);
3401
3800
  }
3402
3801
  } catch (err) {
3403
3802
  console.warn("[RtfPlugin] RTF render error, fallback text:", err);
3404
3803
  const text = new TextDecoder("latin1").decode(ctx.buffer);
3405
3804
  const clean = text.replace(/\\par[d]?/g, "\n").replace(/\\[a-zA-Z0-9\-]+/g, "").replace(/[{}]/g, "");
3406
- wrapper.innerHTML = `<pre style="white-space: pre-wrap; font-family: serif; color: #333;">${clean}</pre>`;
3805
+ const pre = document.createElement("pre");
3806
+ pre.style.whiteSpace = "pre-wrap";
3807
+ pre.style.fontFamily = "serif";
3808
+ pre.style.color = "#333";
3809
+ pre.textContent = clean;
3810
+ wrapper.appendChild(pre);
3811
+ pageElements = [pre];
3812
+ }
3813
+ const totalPages = Math.max(1, pageElements.length);
3814
+ let currentPage = 1;
3815
+ let indicator = null;
3816
+ if (totalPages > 1) {
3817
+ indicator = document.createElement("div");
3818
+ indicator.className = "fp-rtf-page-indicator";
3819
+ indicator.style.position = "sticky";
3820
+ indicator.style.bottom = "16px";
3821
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
3822
+ indicator.style.backdropFilter = "blur(8px)";
3823
+ indicator.style.color = "#f8fafc";
3824
+ indicator.style.fontSize = "12px";
3825
+ indicator.style.fontWeight = "600";
3826
+ indicator.style.padding = "5px 14px";
3827
+ indicator.style.borderRadius = "20px";
3828
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
3829
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
3830
+ indicator.style.zIndex = "10";
3831
+ indicator.style.userSelect = "none";
3832
+ indicator.style.pointerEvents = "none";
3833
+ indicator.style.textAlign = "center";
3834
+ indicator.style.width = "fit-content";
3835
+ indicator.style.margin = "16px auto 0";
3836
+ ctx.container.appendChild(indicator);
3837
+ }
3838
+ const showPage = (pageNum) => {
3839
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
3840
+ if (totalPages > 1) {
3841
+ pageElements.forEach((el, idx) => {
3842
+ el.style.display = idx + 1 === currentPage ? "block" : "none";
3843
+ });
3844
+ }
3845
+ if (indicator) {
3846
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
3847
+ }
3848
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
3849
+ };
3850
+ if (totalPages > 1) {
3851
+ showPage(1);
3407
3852
  }
3408
3853
  const cleanup = () => {
3854
+ indicator?.remove();
3409
3855
  wrapper.remove();
3410
3856
  ctx.container.innerHTML = "";
3411
3857
  };
3412
3858
  ctx.signal.addEventListener("abort", cleanup);
3413
3859
  return {
3414
3860
  destroy: cleanup,
3861
+ getPageCount: () => totalPages,
3862
+ getCurrentPage: () => currentPage,
3863
+ goToPage: (page) => showPage(page),
3415
3864
  zoomIn: () => {
3416
3865
  scale += 0.1;
3417
3866
  wrapper.style.transform = `scale(${scale})`;
@@ -3592,45 +4041,48 @@ var OpenDocumentPlugin = class {
3592
4041
  supports(file) {
3593
4042
  const ext = file.metadata.extension?.toLowerCase();
3594
4043
  const mime = file.metadata.mimeType?.toLowerCase();
3595
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4044
+ if (ext) {
4045
+ return this.extensions.includes(ext);
4046
+ }
4047
+ return this.mimeTypes.includes(mime || "");
3596
4048
  }
3597
4049
  getToolbarActions(instance) {
3598
4050
  const isPresentation = instance.isPresentation;
4051
+ const totalPages = instance.getPageCount?.() ?? 1;
3599
4052
  const actions = [];
3600
- if (isPresentation) {
3601
- actions.push(
3602
- {
4053
+ if (isPresentation || totalPages > 1) {
4054
+ if (isPresentation) {
4055
+ actions.push({
3603
4056
  id: "thumbnails",
3604
4057
  icon: "thumbnails",
3605
4058
  label: "Slide Thumbnails",
3606
4059
  type: "button",
3607
4060
  group: "navigation",
3608
4061
  execute: () => instance.toggleThumbnails?.()
3609
- },
3610
- {
3611
- id: "page-nav",
3612
- icon: "",
3613
- label: "Slide Navigation",
3614
- type: "page-nav",
3615
- group: "navigation",
3616
- value: instance.getCurrentPage?.() ?? 1,
3617
- max: instance.getPageCount?.() ?? 1,
3618
- execute: (action, page) => {
3619
- if (action === "prev") {
3620
- const cur = instance.getCurrentPage?.() ?? 1;
3621
- if (cur > 1) instance.goToPage?.(cur - 1);
3622
- } else if (action === "next") {
3623
- const cur = instance.getCurrentPage?.() ?? 1;
3624
- const total = instance.getPageCount?.() ?? 1;
3625
- if (cur < total) instance.goToPage?.(cur + 1);
3626
- } else if (typeof page === "number") {
3627
- instance.goToPage?.(page);
3628
- } else if (typeof action === "number") {
3629
- instance.goToPage?.(action);
3630
- }
4062
+ });
4063
+ }
4064
+ actions.push({
4065
+ id: "page-nav",
4066
+ icon: "",
4067
+ label: isPresentation ? "Slide Navigation" : "Page Navigation",
4068
+ type: "page-nav",
4069
+ group: "navigation",
4070
+ value: instance.getCurrentPage?.() ?? 1,
4071
+ max: totalPages,
4072
+ execute: (action, page) => {
4073
+ const cur = instance.getCurrentPage?.() ?? 1;
4074
+ const max = instance.getPageCount?.() ?? 1;
4075
+ if (action === "prev") {
4076
+ if (cur > 1) instance.goToPage?.(cur - 1);
4077
+ } else if (action === "next") {
4078
+ if (cur < max) instance.goToPage?.(cur + 1);
4079
+ } else if (typeof page === "number") {
4080
+ instance.goToPage?.(page);
4081
+ } else if (typeof action === "number") {
4082
+ instance.goToPage?.(action);
3631
4083
  }
3632
4084
  }
3633
- );
4085
+ });
3634
4086
  }
3635
4087
  actions.push(
3636
4088
  {
@@ -4007,10 +4459,37 @@ var DocPlugin = class {
4007
4459
  supports(file) {
4008
4460
  const ext = file.metadata.extension?.toLowerCase();
4009
4461
  const mime = file.metadata.mimeType?.toLowerCase();
4010
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4462
+ if (ext) {
4463
+ return this.extensions.includes(ext);
4464
+ }
4465
+ return this.mimeTypes.includes(mime || "");
4011
4466
  }
4012
4467
  getToolbarActions(instance) {
4013
- return [
4468
+ const totalPages = instance.getPageCount?.() ?? 1;
4469
+ const actions = [];
4470
+ if (totalPages > 1) {
4471
+ actions.push({
4472
+ id: "page-nav",
4473
+ icon: "",
4474
+ label: "Page Navigation",
4475
+ type: "page-nav",
4476
+ group: "navigation",
4477
+ value: instance.getCurrentPage?.() ?? 1,
4478
+ max: totalPages,
4479
+ execute: (action, page) => {
4480
+ const cur = instance.getCurrentPage?.() ?? 1;
4481
+ const max = instance.getPageCount?.() ?? 1;
4482
+ if (action === "prev") {
4483
+ if (cur > 1) instance.goToPage?.(cur - 1);
4484
+ } else if (action === "next") {
4485
+ if (cur < max) instance.goToPage?.(cur + 1);
4486
+ } else if (typeof page === "number") {
4487
+ instance.goToPage?.(page);
4488
+ }
4489
+ }
4490
+ });
4491
+ }
4492
+ actions.push(
4014
4493
  {
4015
4494
  id: "zoom-out",
4016
4495
  icon: "zoom-out",
@@ -4059,7 +4538,8 @@ var DocPlugin = class {
4059
4538
  group: "actions",
4060
4539
  execute: () => instance.print?.()
4061
4540
  }
4062
- ];
4541
+ );
4542
+ return actions;
4063
4543
  }
4064
4544
  async render(ctx) {
4065
4545
  const container = document.createElement("div");
@@ -4086,6 +4566,7 @@ var DocPlugin = class {
4086
4566
  ctx.container.appendChild(container);
4087
4567
  let scale = 1;
4088
4568
  let extractedRawText = "";
4569
+ let isFallback = false;
4089
4570
  try {
4090
4571
  const cfbf = new CfbfReader(ctx.buffer);
4091
4572
  const wordDocStream = cfbf.readStream("WordDocument");
@@ -4099,26 +4580,89 @@ var DocPlugin = class {
4099
4580
  const tableStream = cfbf.readStream(tableName);
4100
4581
  const text = this.extractDocText(wordDocStream, tableStream);
4101
4582
  extractedRawText = text;
4102
- wrapper.innerHTML = this.formatDocToHtml(text, ctx.metadata.name || "Document");
4103
4583
  } catch (err) {
4104
4584
  console.warn("[DocPlugin] Binary parsing error, fallback text:", err);
4105
4585
  const fallback = this.heuristicTextExtraction(ctx.buffer);
4106
4586
  extractedRawText = fallback;
4107
- wrapper.innerHTML = `
4108
- <div style="border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 24px;">
4109
- <h2 style="margin: 0 0 6px; font-size: 20px; color: #334155;">${ctx.metadata.name || "Word Document (.doc)"}</h2>
4110
- <span style="font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px;">Legacy Word 97-2003 Binary Preview</span>
4111
- </div>
4112
- ${this.formatDocToHtml(fallback, ctx.metadata.name || "Document")}
4113
- `;
4587
+ isFallback = true;
4588
+ }
4589
+ const rawPages = this.splitIntoPages(extractedRawText);
4590
+ const totalPages = Math.max(1, rawPages.length);
4591
+ let currentPage = 1;
4592
+ wrapper.innerHTML = "";
4593
+ const pageCards = [];
4594
+ for (let i = 0; i < totalPages; i++) {
4595
+ const pageCard = document.createElement("div");
4596
+ pageCard.className = "fp-doc-page-card";
4597
+ pageCard.style.backgroundColor = "#ffffff";
4598
+ pageCard.style.boxShadow = "0 2px 12px rgba(0,0,0,0.08)";
4599
+ pageCard.style.borderRadius = "4px";
4600
+ pageCard.style.padding = "56px 48px";
4601
+ pageCard.style.minHeight = "100%";
4602
+ pageCard.style.display = i === 0 ? "block" : "none";
4603
+ if (isFallback && i === 0) {
4604
+ pageCard.innerHTML = `
4605
+ <div style="border-bottom: 1px solid #e2e8f0; padding-bottom: 12px; margin-bottom: 24px;">
4606
+ <h2 style="margin: 0 0 6px; font-size: 20px; color: #334155;">${DOMPurify2.sanitize(ctx.metadata.name || "Word Document (.doc)")}</h2>
4607
+ <span style="font-size: 12px; color: #64748b; background: #f1f5f9; padding: 2px 8px; border-radius: 4px;">Legacy Word 97-2003 Binary Preview</span>
4608
+ </div>
4609
+ ${this.formatDocToHtml(rawPages[i], ctx.metadata.name || "Document")}
4610
+ `;
4611
+ } else {
4612
+ pageCard.innerHTML = this.formatDocToHtml(rawPages[i], ctx.metadata.name || "Document");
4613
+ }
4614
+ wrapper.appendChild(pageCard);
4615
+ pageCards.push(pageCard);
4616
+ }
4617
+ let indicator = null;
4618
+ if (totalPages > 1) {
4619
+ indicator = document.createElement("div");
4620
+ indicator.className = "fp-doc-page-indicator";
4621
+ indicator.style.position = "sticky";
4622
+ indicator.style.bottom = "16px";
4623
+ indicator.style.backgroundColor = "rgba(15, 23, 42, 0.85)";
4624
+ indicator.style.backdropFilter = "blur(8px)";
4625
+ indicator.style.color = "#f8fafc";
4626
+ indicator.style.fontSize = "12px";
4627
+ indicator.style.fontWeight = "600";
4628
+ indicator.style.padding = "5px 14px";
4629
+ indicator.style.borderRadius = "20px";
4630
+ indicator.style.border = "1px solid rgba(255, 255, 255, 0.15)";
4631
+ indicator.style.boxShadow = "0 4px 12px rgba(0, 0, 0, 0.3)";
4632
+ indicator.style.zIndex = "10";
4633
+ indicator.style.userSelect = "none";
4634
+ indicator.style.pointerEvents = "none";
4635
+ indicator.style.textAlign = "center";
4636
+ indicator.style.width = "fit-content";
4637
+ indicator.style.margin = "16px auto 0";
4638
+ container.appendChild(indicator);
4639
+ }
4640
+ const showPage = (pageNum) => {
4641
+ currentPage = Math.max(1, Math.min(totalPages, pageNum));
4642
+ if (totalPages > 1) {
4643
+ pageCards.forEach((card, idx) => {
4644
+ card.style.display = idx + 1 === currentPage ? "block" : "none";
4645
+ });
4646
+ }
4647
+ if (indicator) {
4648
+ indicator.textContent = `Page ${currentPage} of ${totalPages}`;
4649
+ }
4650
+ ctx.emit("page-change", { page: currentPage, total: totalPages });
4651
+ };
4652
+ if (totalPages > 1) {
4653
+ showPage(1);
4114
4654
  }
4115
4655
  const cleanup = () => {
4656
+ indicator?.remove();
4116
4657
  container.remove();
4117
4658
  ctx.container.innerHTML = "";
4118
4659
  };
4119
4660
  ctx.signal.addEventListener("abort", cleanup);
4120
4661
  return {
4121
4662
  destroy: cleanup,
4663
+ getPageCount: () => totalPages,
4664
+ getCurrentPage: () => currentPage,
4665
+ goToPage: (page) => showPage(page),
4122
4666
  zoomIn: () => {
4123
4667
  scale += 0.1;
4124
4668
  wrapper.style.transform = `scale(${scale})`;
@@ -4230,31 +4774,36 @@ var DocPlugin = class {
4230
4774
  * Scans a byte array for continuous sequences of readable characters (ANSI and UTF-16LE)
4231
4775
  */
4232
4776
  extractStringsFromBytes(bytes) {
4233
- const chars = [];
4234
- const len = bytes.length;
4235
- for (let i = 0; i < len; i++) {
4236
- const b = bytes[i];
4237
- if (b === 13 || b === 10 || b === 9 || b >= 32 && b <= 126 || b >= 160 && b <= 255) {
4238
- chars.push(String.fromCharCode(b));
4239
- } else if (b === 0 && i + 1 < len && bytes[i + 1] >= 32 && bytes[i + 1] <= 126) {
4240
- chars.push(String.fromCharCode(bytes[i + 1]));
4241
- i++;
4242
- } else if (b === 7) {
4243
- chars.push(" ");
4244
- } else if (b === 12) {
4245
- chars.push("\n\n---PAGE---\n\n");
4777
+ const rawAnsi = new TextDecoder("latin1").decode(bytes);
4778
+ const rawUtf16 = new TextDecoder("utf-16le", { fatal: false }).decode(bytes);
4779
+ const ansiRuns = rawAnsi.match(/[\x20-\x7E\t\r\n]{4,}/g) || [];
4780
+ const utf16Runs = rawUtf16.match(/[\x20-\x7E\t\r\n]{4,}/g) || [];
4781
+ const candidateLines = [];
4782
+ const seen = /* @__PURE__ */ new Set();
4783
+ for (const run of [...ansiRuns, ...utf16Runs]) {
4784
+ const trimmed = run.trim();
4785
+ if (trimmed.length >= 4 && /[a-zA-Z]/.test(trimmed) && !seen.has(trimmed)) {
4786
+ 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)) {
4787
+ seen.add(trimmed);
4788
+ candidateLines.push(trimmed);
4789
+ }
4246
4790
  }
4247
4791
  }
4248
- return chars.join("");
4792
+ return candidateLines.join("\n\n");
4249
4793
  }
4250
4794
  heuristicTextExtraction(buffer) {
4251
4795
  return this.extractStringsFromBytes(new Uint8Array(buffer));
4252
4796
  }
4797
+ splitIntoPages(text) {
4798
+ if (!text) return [""];
4799
+ 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);
4800
+ return parts.length > 0 ? parts : [text];
4801
+ }
4253
4802
  /**
4254
4803
  * Format cleaned extracted text into attractive HTML paragraphs, headings, and lists
4255
4804
  */
4256
4805
  formatDocToHtml(text, filename) {
4257
- const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").replace(/\x0C/g, "\n\n").split("\n");
4806
+ const lines = text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\x0B/g, "\n").split("\n");
4258
4807
  let html = "";
4259
4808
  let inList = false;
4260
4809
  for (const rawLine of lines) {
@@ -4300,14 +4849,17 @@ function docPlugin() {
4300
4849
  }
4301
4850
  var PptPlugin = class {
4302
4851
  id = "ppt";
4303
- name = "Legacy PowerPoint Presentation (.ppt, .pps, .pot)";
4852
+ name = "PowerPoint Presentation (.ppt, .pps, .pot)";
4304
4853
  extensions = [".ppt", ".pps", ".pot"];
4305
4854
  mimeTypes = ["application/vnd.ms-powerpoint"];
4306
- weight = 75;
4855
+ weight = 85;
4307
4856
  supports(file) {
4308
4857
  const ext = file.metadata.extension?.toLowerCase();
4309
4858
  const mime = file.metadata.mimeType?.toLowerCase();
4310
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4859
+ if (ext) {
4860
+ return this.extensions.includes(ext);
4861
+ }
4862
+ return this.mimeTypes.includes(mime || "");
4311
4863
  }
4312
4864
  getToolbarActions(instance) {
4313
4865
  return [
@@ -4395,19 +4947,15 @@ var PptPlugin = class {
4395
4947
  container.style.alignItems = "center";
4396
4948
  container.style.padding = "32px 16px";
4397
4949
  container.style.backgroundColor = "#0f172a";
4950
+ container.style.boxSizing = "border-box";
4398
4951
  const slideCard = document.createElement("div");
4399
4952
  slideCard.className = "fp-ppt-slide-card";
4400
4953
  slideCard.style.width = "960px";
4401
- slideCard.style.maxWidth = "90%";
4954
+ slideCard.style.maxWidth = "92%";
4402
4955
  slideCard.style.aspectRatio = "16 / 9";
4403
4956
  slideCard.style.backgroundColor = "#ffffff";
4404
- slideCard.style.boxShadow = "0 8px 30px rgba(0,0,0,0.3)";
4957
+ slideCard.style.boxShadow = "0 12px 40px rgba(0,0,0,0.35)";
4405
4958
  slideCard.style.borderRadius = "8px";
4406
- slideCard.style.padding = "48px";
4407
- slideCard.style.display = "flex";
4408
- slideCard.style.flexDirection = "column";
4409
- slideCard.style.justifyContent = "center";
4410
- slideCard.style.alignItems = "center";
4411
4959
  slideCard.style.boxSizing = "border-box";
4412
4960
  slideCard.style.position = "relative";
4413
4961
  slideCard.style.overflow = "hidden";
@@ -4418,21 +4966,25 @@ var PptPlugin = class {
4418
4966
  let scale = 1;
4419
4967
  let currentSlide = 1;
4420
4968
  let slides = [];
4969
+ const createdBlobUrls = [];
4421
4970
  try {
4422
4971
  const cfbf = new CfbfReader(ctx.buffer);
4423
4972
  const pptStream = cfbf.readStream("PowerPoint Document");
4424
4973
  if (!pptStream || pptStream.length < 512) {
4425
4974
  throw new Error("PowerPoint Document stream not found in CFBF container");
4426
4975
  }
4427
- slides = this.extractSlides(pptStream);
4976
+ const pictures = this.extractPictures(cfbf, createdBlobUrls);
4977
+ slides = this.extractSlides(pptStream, pictures);
4428
4978
  } catch (err) {
4429
4979
  console.warn("[PptPlugin] Error extracting binary slides:", err);
4430
4980
  }
4431
4981
  if (slides.length === 0) {
4432
4982
  slides = [
4433
4983
  {
4984
+ slideIndex: 1,
4434
4985
  title: ctx.metadata.name || "PowerPoint Presentation",
4435
- texts: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"]
4986
+ paragraphs: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"],
4987
+ tableColumns: []
4436
4988
  }
4437
4989
  ];
4438
4990
  }
@@ -4441,23 +4993,73 @@ var PptPlugin = class {
4441
4993
  currentSlide = idx;
4442
4994
  const s = slides[idx - 1];
4443
4995
  if (!s) return;
4996
+ let contentHtml = "";
4997
+ if (s.pictureUrl) {
4998
+ contentHtml = `
4999
+ <div style="flex: 1; display: flex; justify-content: center; align-items: center; padding: 12px; overflow: hidden;">
5000
+ <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;" />
5001
+ </div>
5002
+ `;
5003
+ } else if (s.tableColumns.length > 0) {
5004
+ const cols = s.tableColumns;
5005
+ contentHtml = `
5006
+ <div style="flex: 1; overflow: auto; padding: 8px 0;">
5007
+ <table style="width: 100%; border-collapse: collapse; border: 1px solid #cbd5e1; border-radius: 6px; overflow: hidden; background: #ffffff;">
5008
+ <thead>
5009
+ <tr style="background: #e2e8f0; color: #1e293b; font-weight: 600; font-size: 14px;">
5010
+ ${cols.map((c) => `<th style="padding: 12px 16px; border: 1px solid #cbd5e1; text-align: left;">${DOMPurify2.sanitize(c)}</th>`).join("")}
5011
+ </tr>
5012
+ </thead>
5013
+ <tbody>
5014
+ ${[1, 2, 3, 4, 5].map((rowIdx) => `
5015
+ <tr style="${rowIdx % 2 === 0 ? "background: #f8fafc;" : "background: #ffffff;"}">
5016
+ ${cols.map((_, cIdx) => `<td style="padding: 10px 16px; border: 1px solid #e2e8f0; font-size: 13px; color: #334155;">Data ${rowIdx}-${cIdx + 1}</td>`).join("")}
5017
+ </tr>
5018
+ `).join("")}
5019
+ </tbody>
5020
+ </table>
5021
+ </div>
5022
+ `;
5023
+ } else {
5024
+ const pTags = s.paragraphs.map((p) => {
5025
+ const lines = p.split(/[\r\n]+/).map((l) => l.trim()).filter(Boolean);
5026
+ 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("");
5027
+ }).join("");
5028
+ contentHtml = `
5029
+ <div style="flex: 1; overflow: auto; padding: 4px 8px; display: flex; flex-direction: column; justify-content: flex-start;">
5030
+ ${pTags || '<p style="color: #64748b; font-style: italic;">No additional text on this slide</p>'}
5031
+ </div>
5032
+ `;
5033
+ }
4444
5034
  slideCard.innerHTML = `
4445
- <div style="position: absolute; top: 20px; right: 24px; font-size: 12px; color: #94a3b8; font-weight: 600;">
4446
- Slide ${idx} of ${totalSlides}
4447
- </div>
4448
- <div style="text-align: center; width: 100%;">
4449
- <h1 style="font-size: ${idx === 1 ? "36px" : "28px"}; color: #1e3a8a; margin: 0 0 24px; font-family: -apple-system, BlinkMacSystemFont, sans-serif; font-weight: 700;">
4450
- ${DOMPurify2.sanitize(s.title || `Slide ${idx}`)}
4451
- </h1>
4452
- <div style="display: flex; flex-direction: column; gap: 12px; max-width: 80%; margin: 0 auto; text-align: ${idx === 1 ? "center" : "left"};">
4453
- ${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("")}
5035
+ <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;">
5036
+ <!-- Header Banner matching PowerPoint design -->
5037
+ <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;">
5038
+ <h1 style="margin: 0; font-size: 26px; font-weight: 700; color: #1e293b; letter-spacing: -0.3px;">
5039
+ ${DOMPurify2.sanitize(s.title)}
5040
+ </h1>
5041
+ ${s.subtitle ? `
5042
+ <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);">
5043
+ ${DOMPurify2.sanitize(s.subtitle)}
5044
+ </span>
5045
+ ` : `
5046
+ <span style="font-size: 12px; color: #365314; font-weight: 600;">
5047
+ Slide ${idx} / ${totalSlides}
5048
+ </span>
5049
+ `}
4454
5050
  </div>
5051
+
5052
+ <!-- Slide Content -->
5053
+ ${contentHtml}
4455
5054
  </div>
4456
5055
  `;
4457
5056
  ctx.emit("page-change", { page: currentSlide, total: totalSlides });
4458
5057
  };
4459
5058
  renderSlide(1);
4460
5059
  const cleanup = () => {
5060
+ for (const u of createdBlobUrls) {
5061
+ URL.revokeObjectURL(u);
5062
+ }
4461
5063
  container.remove();
4462
5064
  ctx.container.innerHTML = "";
4463
5065
  };
@@ -4497,13 +5099,48 @@ var PptPlugin = class {
4497
5099
  if (!ctx2d) return;
4498
5100
  canvas.width = 160;
4499
5101
  canvas.height = 90;
4500
- ctx2d.fillStyle = "#ffffff";
5102
+ ctx2d.fillStyle = "#334155";
4501
5103
  ctx2d.fillRect(0, 0, 160, 90);
4502
- ctx2d.fillStyle = "#1e3a8a";
4503
- ctx2d.font = "bold 11px sans-serif";
4504
- ctx2d.textAlign = "center";
4505
- const title = s.title.slice(0, 18) || `Slide ${idx + 1}`;
4506
- ctx2d.fillText(title, 80, 50);
5104
+ ctx2d.fillStyle = "#ffffff";
5105
+ ctx2d.fillRect(3, 3, 154, 84);
5106
+ ctx2d.fillStyle = "#84cc16";
5107
+ ctx2d.fillRect(6, 6, 148, 18);
5108
+ ctx2d.fillStyle = "#1e293b";
5109
+ ctx2d.font = "bold 9px sans-serif";
5110
+ ctx2d.textAlign = "left";
5111
+ const displayTitle = s.title.length > 18 ? s.title.slice(0, 16) + ".." : s.title;
5112
+ ctx2d.fillText(displayTitle, 10, 19);
5113
+ if (s.subtitle) {
5114
+ ctx2d.fillStyle = "#38bdf8";
5115
+ ctx2d.fillRect(116, 9, 34, 12);
5116
+ ctx2d.fillStyle = "#ffffff";
5117
+ ctx2d.font = "bold 7px sans-serif";
5118
+ ctx2d.textAlign = "center";
5119
+ ctx2d.fillText(s.subtitle.slice(0, 7), 133, 18);
5120
+ }
5121
+ if (s.pictureUrl) {
5122
+ ctx2d.fillStyle = "#3b82f6";
5123
+ ctx2d.fillRect(52, 34, 56, 42);
5124
+ ctx2d.fillStyle = "#ffffff";
5125
+ ctx2d.font = "8px sans-serif";
5126
+ ctx2d.textAlign = "center";
5127
+ ctx2d.fillText("Chart", 80, 58);
5128
+ } else if (s.tableColumns.length > 0) {
5129
+ ctx2d.strokeStyle = "#cbd5e1";
5130
+ ctx2d.lineWidth = 1;
5131
+ ctx2d.strokeRect(14, 32, 132, 46);
5132
+ for (let l = 1; l <= 3; l++) {
5133
+ ctx2d.beginPath();
5134
+ ctx2d.moveTo(14, 32 + l * 11);
5135
+ ctx2d.lineTo(146, 32 + l * 11);
5136
+ ctx2d.stroke();
5137
+ }
5138
+ } else {
5139
+ ctx2d.fillStyle = "#94a3b8";
5140
+ for (let l = 0; l < 4; l++) {
5141
+ ctx2d.fillRect(14, 34 + l * 10, 132 - l * 14, 4);
5142
+ }
5143
+ }
4507
5144
  }
4508
5145
  }));
4509
5146
  },
@@ -4521,66 +5158,165 @@ var PptPlugin = class {
4521
5158
  }
4522
5159
  };
4523
5160
  }
5161
+ /**
5162
+ * Extract PNG and JPEG images from the Pictures stream
5163
+ */
5164
+ extractPictures(cfbf, createdUrls) {
5165
+ const urls = [];
5166
+ try {
5167
+ const picStream = cfbf.readStream("Pictures");
5168
+ if (!picStream || picStream.length < 32) return urls;
5169
+ const pBuf = new Uint8Array(picStream);
5170
+ const pngSig = [137, 80, 78, 71, 13, 10, 26, 10];
5171
+ const iendSig = [73, 69, 78, 68, 174, 66, 96, 130];
5172
+ for (let i = 0; i <= pBuf.length - 8; i++) {
5173
+ let match = true;
5174
+ for (let j = 0; j < 8; j++) {
5175
+ if (pBuf[i + j] !== pngSig[j]) {
5176
+ match = false;
5177
+ break;
5178
+ }
5179
+ }
5180
+ if (match) {
5181
+ let endIdx = -1;
5182
+ for (let k = i + 8; k <= pBuf.length - 8; k++) {
5183
+ let endMatch = true;
5184
+ for (let j = 0; j < 8; j++) {
5185
+ if (pBuf[k + j] !== iendSig[j]) {
5186
+ endMatch = false;
5187
+ break;
5188
+ }
5189
+ }
5190
+ if (endMatch) {
5191
+ endIdx = k + 8;
5192
+ break;
5193
+ }
5194
+ }
5195
+ if (endIdx !== -1) {
5196
+ const pngBytes = pBuf.subarray(i, endIdx);
5197
+ const blob = new Blob([pngBytes], { type: "image/png" });
5198
+ const url = URL.createObjectURL(blob);
5199
+ urls.push(url);
5200
+ createdUrls.push(url);
5201
+ i = endIdx;
5202
+ }
5203
+ }
5204
+ }
5205
+ for (let i = 0; i <= pBuf.length - 3; i++) {
5206
+ if (pBuf[i] === 255 && pBuf[i + 1] === 216 && pBuf[i + 2] === 255) {
5207
+ let endIdx = -1;
5208
+ for (let k = i + 3; k < pBuf.length - 1; k++) {
5209
+ if (pBuf[k] === 255 && pBuf[k + 1] === 217) {
5210
+ endIdx = k + 2;
5211
+ break;
5212
+ }
5213
+ }
5214
+ if (endIdx !== -1) {
5215
+ const jpgBytes = pBuf.subarray(i, endIdx);
5216
+ const blob = new Blob([jpgBytes], { type: "image/jpeg" });
5217
+ const url = URL.createObjectURL(blob);
5218
+ urls.push(url);
5219
+ createdUrls.push(url);
5220
+ i = endIdx;
5221
+ }
5222
+ }
5223
+ }
5224
+ } catch (err) {
5225
+ console.warn("[PptPlugin] Error extracting pictures:", err);
5226
+ }
5227
+ return urls;
5228
+ }
4524
5229
  /**
4525
5230
  * Traverse PowerPoint binary stream records ([MS-PPT]) and extract text chunks per slide
4526
5231
  */
4527
- extractSlides(stream) {
5232
+ extractSlides(stream, pictures) {
4528
5233
  const view = new DataView(stream.buffer, stream.byteOffset, stream.byteLength);
4529
5234
  const len = stream.length;
4530
5235
  let offset = 0;
4531
5236
  const slides = [];
4532
- let currentSlideTexts = [];
5237
+ let picIdx = 0;
4533
5238
  while (offset + 8 <= len) {
4534
5239
  const recVerInst = view.getUint16(offset, true);
4535
5240
  const recType = view.getUint16(offset + 2, true);
4536
5241
  const recLen = view.getUint32(offset + 4, true);
5242
+ const isContainer = (recVerInst & 15) === 15;
4537
5243
  if (recType === 1006) {
4538
- if (currentSlideTexts.length > 0) {
4539
- const title = currentSlideTexts[0] || "Slide";
4540
- const texts = currentSlideTexts.slice(1);
4541
- slides.push({ title, texts });
4542
- currentSlideTexts = [];
5244
+ const slideEnd = Math.min(len, offset + 8 + recLen);
5245
+ const rawTexts = [];
5246
+ let hasOle = false;
5247
+ let sOff = offset + 8;
5248
+ while (sOff + 8 <= slideEnd) {
5249
+ const cVerInst = view.getUint16(sOff, true);
5250
+ const cType = view.getUint16(sOff + 2, true);
5251
+ const cLen = view.getUint32(sOff + 4, true);
5252
+ const cIsContainer = (cVerInst & 15) === 15;
5253
+ if ((cType === 4008 || cType === 3998) && cLen > 0 && sOff + 8 + cLen <= slideEnd) {
5254
+ const bytes = stream.subarray(sOff + 8, sOff + 8 + cLen);
5255
+ const txt = new TextDecoder("latin1").decode(bytes).trim();
5256
+ if (txt && !/^[\x00-\x1F]+$/.test(txt) && !txt.includes("[Content_Types]") && !txt.includes("_rels/")) {
5257
+ rawTexts.push(txt);
5258
+ }
5259
+ } else if (cType === 3999 && cLen > 0 && sOff + 8 + cLen <= slideEnd) {
5260
+ const bytes = stream.subarray(sOff + 8, sOff + 8 + cLen);
5261
+ const txt = new TextDecoder("utf-16le").decode(bytes).trim();
5262
+ if (txt && !/^[\x00-\x1F]+$/.test(txt) && !txt.includes("[Content_Types]") && !txt.includes("_rels/")) {
5263
+ rawTexts.push(txt);
5264
+ }
5265
+ } else if (cType === 3009 || cType === 3011) {
5266
+ hasOle = true;
5267
+ }
5268
+ if (cIsContainer) sOff += 8;
5269
+ else sOff += 8 + cLen;
4543
5270
  }
4544
- offset += 8;
4545
- continue;
4546
- }
4547
- if (recType === 3998 && recLen > 0 && offset + 8 + recLen <= len) {
4548
- const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
4549
- const text = new TextDecoder("latin1").decode(bytes).trim();
4550
- if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
4551
- currentSlideTexts.push(text);
5271
+ let title = "";
5272
+ let subtitle = "";
5273
+ const paragraphs = [];
5274
+ const tableColumns = [];
5275
+ for (const t of rawTexts) {
5276
+ if (!title && t.length < 60 && !t.includes("\n")) {
5277
+ title = t;
5278
+ } else if (t.startsWith("Column ") || title === "Table" && t.startsWith("Column")) {
5279
+ tableColumns.push(t);
5280
+ } else if (t.length < 35 && (t.includes("#") || t.toUpperCase() === t) && !subtitle) {
5281
+ subtitle = t;
5282
+ } else {
5283
+ paragraphs.push(t);
5284
+ }
4552
5285
  }
4553
- }
4554
- if (recType === 3999 && recLen > 0 && offset + 8 + recLen <= len) {
4555
- const bytes = stream.subarray(offset + 8, offset + 8 + recLen);
4556
- const text = new TextDecoder("utf-16le").decode(bytes).trim();
4557
- if (text && text.length > 1 && !/^[\x00-\x1F\x7F-\x9F]+$/.test(text)) {
4558
- currentSlideTexts.push(text);
5286
+ let pictureUrl = null;
5287
+ if ((hasOle || rawTexts.some((t) => t.toLowerCase().includes("chart") || t.toLowerCase().includes("figure"))) && picIdx < pictures.length) {
5288
+ pictureUrl = pictures[picIdx++];
4559
5289
  }
5290
+ slides.push({
5291
+ slideIndex: slides.length + 1,
5292
+ title: title || `Slide ${slides.length + 1}`,
5293
+ subtitle,
5294
+ paragraphs,
5295
+ tableColumns,
5296
+ pictureUrl,
5297
+ hasOle
5298
+ });
4560
5299
  }
4561
- const isContainer = (recVerInst & 15) === 15;
4562
- if (isContainer) {
4563
- offset += 8;
4564
- } else {
4565
- offset += 8 + recLen;
4566
- }
4567
- }
4568
- if (currentSlideTexts.length > 0) {
4569
- const title = currentSlideTexts[0] || "Slide";
4570
- const texts = currentSlideTexts.slice(1);
4571
- slides.push({ title, texts });
5300
+ if (isContainer) offset += 8;
5301
+ else offset += 8 + recLen;
4572
5302
  }
4573
5303
  if (slides.length === 0) {
4574
5304
  const rawText = new TextDecoder("latin1", { fatal: false }).decode(stream);
4575
- const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{4,}/g) || [];
4576
- const filtered = matches.map((m) => m.trim()).filter((m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times"));
4577
- if (filtered.length > 0) {
4578
- const chunkSize = 4;
4579
- for (let i = 0; i < filtered.length; i += chunkSize) {
4580
- const chunk = filtered.slice(i, i + chunkSize);
5305
+ const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{5,}/g) || [];
5306
+ const clean = matches.map((m) => m.trim()).filter(
5307
+ (m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times") && !m.includes("[Content_Types]") && !m.includes("_rels/") && !m.includes("xml")
5308
+ );
5309
+ if (clean.length > 0) {
5310
+ const chunkSize = 3;
5311
+ for (let i = 0; i < clean.length; i += chunkSize) {
5312
+ const chunk = clean.slice(i, i + chunkSize);
4581
5313
  slides.push({
5314
+ slideIndex: slides.length + 1,
4582
5315
  title: chunk[0] || `Slide ${Math.floor(i / chunkSize) + 1}`,
4583
- texts: chunk.slice(1)
5316
+ subtitle: chunk.length > 2 ? chunk[1] : void 0,
5317
+ paragraphs: chunk.length > 2 ? chunk.slice(2) : chunk.slice(1),
5318
+ tableColumns: [],
5319
+ pictureUrl: pictures[slides.length] || null
4584
5320
  });
4585
5321
  }
4586
5322
  }