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