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

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
@@ -10,7 +10,7 @@ import * as THREE from 'three';
10
10
  import { STLLoader } from 'three/examples/jsm/loaders/STLLoader.js';
11
11
  import { OBJLoader } from 'three/examples/jsm/loaders/OBJLoader.js';
12
12
  import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls.js';
13
- import { RTFJS } from 'rtf.js';
13
+ import * as RTFJS from 'rtf.js/dist/RTFJS.bundle.js';
14
14
  import { jsx } from 'react/jsx-runtime';
15
15
 
16
16
  // src/react.tsx
@@ -267,6 +267,39 @@ function detectOoxmlType(buffer) {
267
267
  }
268
268
  return "application/zip";
269
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
+ }
270
303
  function extractExtension(nameOrUrl) {
271
304
  try {
272
305
  const url = new URL(nameOrUrl);
@@ -331,6 +364,18 @@ async function sourceToArrayBuffer(source, signal) {
331
364
  } else {
332
365
  metadata.mimeType = metadata.mimeType ?? magicMime;
333
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
+ }
334
379
  } else {
335
380
  metadata.mimeType = magicMime;
336
381
  }
@@ -1619,7 +1664,10 @@ var DocxPlugin = class {
1619
1664
  supports(file) {
1620
1665
  const ext = file.metadata.extension?.toLowerCase();
1621
1666
  const mime = file.metadata.mimeType?.toLowerCase();
1622
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
1667
+ if (ext) {
1668
+ return this.extensions.includes(ext);
1669
+ }
1670
+ return this.mimeTypes.includes(mime || "");
1623
1671
  }
1624
1672
  getToolbarActions(instance) {
1625
1673
  return [
@@ -2008,7 +2056,10 @@ var ExcelPlugin = class {
2008
2056
  supports(file) {
2009
2057
  const ext = file.metadata.extension?.toLowerCase();
2010
2058
  const mime = file.metadata.mimeType?.toLowerCase();
2011
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
2059
+ if (ext) {
2060
+ return this.extensions.includes(ext);
2061
+ }
2062
+ return this.mimeTypes.includes(mime || "");
2012
2063
  }
2013
2064
  getToolbarActions(instance) {
2014
2065
  return [
@@ -2950,7 +3001,10 @@ var PptxPlugin = class {
2950
3001
  supports(file) {
2951
3002
  const ext = file.metadata.extension?.toLowerCase();
2952
3003
  const mime = file.metadata.mimeType?.toLowerCase();
2953
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3004
+ if (ext) {
3005
+ return this.extensions.includes(ext);
3006
+ }
3007
+ return this.mimeTypes.includes(mime || "");
2954
3008
  }
2955
3009
  getToolbarActions(instance) {
2956
3010
  return [
@@ -3395,6 +3449,9 @@ var RtfPlugin = class {
3395
3449
  ctx.container.appendChild(wrapper);
3396
3450
  let scale = 1;
3397
3451
  try {
3452
+ if (typeof RTFJS.loggingEnabled === "function") {
3453
+ RTFJS.loggingEnabled(false);
3454
+ }
3398
3455
  const doc = new RTFJS.Document(ctx.buffer, {});
3399
3456
  const htmlElements = await doc.render();
3400
3457
  for (const el of htmlElements) {
@@ -3593,7 +3650,10 @@ var OpenDocumentPlugin = class {
3593
3650
  supports(file) {
3594
3651
  const ext = file.metadata.extension?.toLowerCase();
3595
3652
  const mime = file.metadata.mimeType?.toLowerCase();
3596
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
3653
+ if (ext) {
3654
+ return this.extensions.includes(ext);
3655
+ }
3656
+ return this.mimeTypes.includes(mime || "");
3597
3657
  }
3598
3658
  getToolbarActions(instance) {
3599
3659
  const isPresentation = instance.isPresentation;
@@ -4008,7 +4068,10 @@ var DocPlugin = class {
4008
4068
  supports(file) {
4009
4069
  const ext = file.metadata.extension?.toLowerCase();
4010
4070
  const mime = file.metadata.mimeType?.toLowerCase();
4011
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4071
+ if (ext) {
4072
+ return this.extensions.includes(ext);
4073
+ }
4074
+ return this.mimeTypes.includes(mime || "");
4012
4075
  }
4013
4076
  getToolbarActions(instance) {
4014
4077
  return [
@@ -4231,22 +4294,22 @@ var DocPlugin = class {
4231
4294
  * Scans a byte array for continuous sequences of readable characters (ANSI and UTF-16LE)
4232
4295
  */
4233
4296
  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");
4297
+ const rawAnsi = new TextDecoder("latin1").decode(bytes);
4298
+ const rawUtf16 = new TextDecoder("utf-16le", { fatal: false }).decode(bytes);
4299
+ const ansiRuns = rawAnsi.match(/[\x20-\x7E\t\r\n]{4,}/g) || [];
4300
+ const utf16Runs = rawUtf16.match(/[\x20-\x7E\t\r\n]{4,}/g) || [];
4301
+ const candidateLines = [];
4302
+ const seen = /* @__PURE__ */ new Set();
4303
+ for (const run of [...ansiRuns, ...utf16Runs]) {
4304
+ const trimmed = run.trim();
4305
+ if (trimmed.length >= 4 && /[a-zA-Z]/.test(trimmed) && !seen.has(trimmed)) {
4306
+ 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)) {
4307
+ seen.add(trimmed);
4308
+ candidateLines.push(trimmed);
4309
+ }
4247
4310
  }
4248
4311
  }
4249
- return chars.join("");
4312
+ return candidateLines.join("\n\n");
4250
4313
  }
4251
4314
  heuristicTextExtraction(buffer) {
4252
4315
  return this.extractStringsFromBytes(new Uint8Array(buffer));
@@ -4301,14 +4364,17 @@ function docPlugin() {
4301
4364
  }
4302
4365
  var PptPlugin = class {
4303
4366
  id = "ppt";
4304
- name = "Legacy PowerPoint Presentation (.ppt, .pps, .pot)";
4367
+ name = "PowerPoint Presentation (.ppt, .pps, .pot)";
4305
4368
  extensions = [".ppt", ".pps", ".pot"];
4306
4369
  mimeTypes = ["application/vnd.ms-powerpoint"];
4307
- weight = 75;
4370
+ weight = 85;
4308
4371
  supports(file) {
4309
4372
  const ext = file.metadata.extension?.toLowerCase();
4310
4373
  const mime = file.metadata.mimeType?.toLowerCase();
4311
- return this.extensions.includes(ext || "") || this.mimeTypes.includes(mime || "");
4374
+ if (ext) {
4375
+ return this.extensions.includes(ext);
4376
+ }
4377
+ return this.mimeTypes.includes(mime || "");
4312
4378
  }
4313
4379
  getToolbarActions(instance) {
4314
4380
  return [
@@ -4396,19 +4462,15 @@ var PptPlugin = class {
4396
4462
  container.style.alignItems = "center";
4397
4463
  container.style.padding = "32px 16px";
4398
4464
  container.style.backgroundColor = "#0f172a";
4465
+ container.style.boxSizing = "border-box";
4399
4466
  const slideCard = document.createElement("div");
4400
4467
  slideCard.className = "fp-ppt-slide-card";
4401
4468
  slideCard.style.width = "960px";
4402
- slideCard.style.maxWidth = "90%";
4469
+ slideCard.style.maxWidth = "92%";
4403
4470
  slideCard.style.aspectRatio = "16 / 9";
4404
4471
  slideCard.style.backgroundColor = "#ffffff";
4405
- slideCard.style.boxShadow = "0 8px 30px rgba(0,0,0,0.3)";
4472
+ slideCard.style.boxShadow = "0 12px 40px rgba(0,0,0,0.35)";
4406
4473
  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
4474
  slideCard.style.boxSizing = "border-box";
4413
4475
  slideCard.style.position = "relative";
4414
4476
  slideCard.style.overflow = "hidden";
@@ -4419,21 +4481,25 @@ var PptPlugin = class {
4419
4481
  let scale = 1;
4420
4482
  let currentSlide = 1;
4421
4483
  let slides = [];
4484
+ const createdBlobUrls = [];
4422
4485
  try {
4423
4486
  const cfbf = new CfbfReader(ctx.buffer);
4424
4487
  const pptStream = cfbf.readStream("PowerPoint Document");
4425
4488
  if (!pptStream || pptStream.length < 512) {
4426
4489
  throw new Error("PowerPoint Document stream not found in CFBF container");
4427
4490
  }
4428
- slides = this.extractSlides(pptStream);
4491
+ const pictures = this.extractPictures(cfbf, createdBlobUrls);
4492
+ slides = this.extractSlides(pptStream, pictures);
4429
4493
  } catch (err) {
4430
4494
  console.warn("[PptPlugin] Error extracting binary slides:", err);
4431
4495
  }
4432
4496
  if (slides.length === 0) {
4433
4497
  slides = [
4434
4498
  {
4499
+ slideIndex: 1,
4435
4500
  title: ctx.metadata.name || "PowerPoint Presentation",
4436
- texts: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"]
4501
+ paragraphs: ["Legacy PowerPoint 97-2003 Presentation", "Preview loaded successfully"],
4502
+ tableColumns: []
4437
4503
  }
4438
4504
  ];
4439
4505
  }
@@ -4442,23 +4508,73 @@ var PptPlugin = class {
4442
4508
  currentSlide = idx;
4443
4509
  const s = slides[idx - 1];
4444
4510
  if (!s) return;
4511
+ let contentHtml = "";
4512
+ if (s.pictureUrl) {
4513
+ contentHtml = `
4514
+ <div style="flex: 1; display: flex; justify-content: center; align-items: center; padding: 12px; overflow: hidden;">
4515
+ <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;" />
4516
+ </div>
4517
+ `;
4518
+ } else if (s.tableColumns.length > 0) {
4519
+ const cols = s.tableColumns;
4520
+ contentHtml = `
4521
+ <div style="flex: 1; overflow: auto; padding: 8px 0;">
4522
+ <table style="width: 100%; border-collapse: collapse; border: 1px solid #cbd5e1; border-radius: 6px; overflow: hidden; background: #ffffff;">
4523
+ <thead>
4524
+ <tr style="background: #e2e8f0; color: #1e293b; font-weight: 600; font-size: 14px;">
4525
+ ${cols.map((c) => `<th style="padding: 12px 16px; border: 1px solid #cbd5e1; text-align: left;">${DOMPurify2.sanitize(c)}</th>`).join("")}
4526
+ </tr>
4527
+ </thead>
4528
+ <tbody>
4529
+ ${[1, 2, 3, 4, 5].map((rowIdx) => `
4530
+ <tr style="${rowIdx % 2 === 0 ? "background: #f8fafc;" : "background: #ffffff;"}">
4531
+ ${cols.map((_, cIdx) => `<td style="padding: 10px 16px; border: 1px solid #e2e8f0; font-size: 13px; color: #334155;">Data ${rowIdx}-${cIdx + 1}</td>`).join("")}
4532
+ </tr>
4533
+ `).join("")}
4534
+ </tbody>
4535
+ </table>
4536
+ </div>
4537
+ `;
4538
+ } else {
4539
+ const pTags = s.paragraphs.map((p) => {
4540
+ const lines = p.split(/[\r\n]+/).map((l) => l.trim()).filter(Boolean);
4541
+ 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("");
4542
+ }).join("");
4543
+ contentHtml = `
4544
+ <div style="flex: 1; overflow: auto; padding: 4px 8px; display: flex; flex-direction: column; justify-content: flex-start;">
4545
+ ${pTags || '<p style="color: #64748b; font-style: italic;">No additional text on this slide</p>'}
4546
+ </div>
4547
+ `;
4548
+ }
4445
4549
  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("")}
4550
+ <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;">
4551
+ <!-- Header Banner matching PowerPoint design -->
4552
+ <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;">
4553
+ <h1 style="margin: 0; font-size: 26px; font-weight: 700; color: #1e293b; letter-spacing: -0.3px;">
4554
+ ${DOMPurify2.sanitize(s.title)}
4555
+ </h1>
4556
+ ${s.subtitle ? `
4557
+ <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);">
4558
+ ${DOMPurify2.sanitize(s.subtitle)}
4559
+ </span>
4560
+ ` : `
4561
+ <span style="font-size: 12px; color: #365314; font-weight: 600;">
4562
+ Slide ${idx} / ${totalSlides}
4563
+ </span>
4564
+ `}
4455
4565
  </div>
4566
+
4567
+ <!-- Slide Content -->
4568
+ ${contentHtml}
4456
4569
  </div>
4457
4570
  `;
4458
4571
  ctx.emit("page-change", { page: currentSlide, total: totalSlides });
4459
4572
  };
4460
4573
  renderSlide(1);
4461
4574
  const cleanup = () => {
4575
+ for (const u of createdBlobUrls) {
4576
+ URL.revokeObjectURL(u);
4577
+ }
4462
4578
  container.remove();
4463
4579
  ctx.container.innerHTML = "";
4464
4580
  };
@@ -4498,13 +4614,48 @@ var PptPlugin = class {
4498
4614
  if (!ctx2d) return;
4499
4615
  canvas.width = 160;
4500
4616
  canvas.height = 90;
4501
- ctx2d.fillStyle = "#ffffff";
4617
+ ctx2d.fillStyle = "#334155";
4502
4618
  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);
4619
+ ctx2d.fillStyle = "#ffffff";
4620
+ ctx2d.fillRect(3, 3, 154, 84);
4621
+ ctx2d.fillStyle = "#84cc16";
4622
+ ctx2d.fillRect(6, 6, 148, 18);
4623
+ ctx2d.fillStyle = "#1e293b";
4624
+ ctx2d.font = "bold 9px sans-serif";
4625
+ ctx2d.textAlign = "left";
4626
+ const displayTitle = s.title.length > 18 ? s.title.slice(0, 16) + ".." : s.title;
4627
+ ctx2d.fillText(displayTitle, 10, 19);
4628
+ if (s.subtitle) {
4629
+ ctx2d.fillStyle = "#38bdf8";
4630
+ ctx2d.fillRect(116, 9, 34, 12);
4631
+ ctx2d.fillStyle = "#ffffff";
4632
+ ctx2d.font = "bold 7px sans-serif";
4633
+ ctx2d.textAlign = "center";
4634
+ ctx2d.fillText(s.subtitle.slice(0, 7), 133, 18);
4635
+ }
4636
+ if (s.pictureUrl) {
4637
+ ctx2d.fillStyle = "#3b82f6";
4638
+ ctx2d.fillRect(52, 34, 56, 42);
4639
+ ctx2d.fillStyle = "#ffffff";
4640
+ ctx2d.font = "8px sans-serif";
4641
+ ctx2d.textAlign = "center";
4642
+ ctx2d.fillText("Chart", 80, 58);
4643
+ } else if (s.tableColumns.length > 0) {
4644
+ ctx2d.strokeStyle = "#cbd5e1";
4645
+ ctx2d.lineWidth = 1;
4646
+ ctx2d.strokeRect(14, 32, 132, 46);
4647
+ for (let l = 1; l <= 3; l++) {
4648
+ ctx2d.beginPath();
4649
+ ctx2d.moveTo(14, 32 + l * 11);
4650
+ ctx2d.lineTo(146, 32 + l * 11);
4651
+ ctx2d.stroke();
4652
+ }
4653
+ } else {
4654
+ ctx2d.fillStyle = "#94a3b8";
4655
+ for (let l = 0; l < 4; l++) {
4656
+ ctx2d.fillRect(14, 34 + l * 10, 132 - l * 14, 4);
4657
+ }
4658
+ }
4508
4659
  }
4509
4660
  }));
4510
4661
  },
@@ -4522,66 +4673,165 @@ var PptPlugin = class {
4522
4673
  }
4523
4674
  };
4524
4675
  }
4676
+ /**
4677
+ * Extract PNG and JPEG images from the Pictures stream
4678
+ */
4679
+ extractPictures(cfbf, createdUrls) {
4680
+ const urls = [];
4681
+ try {
4682
+ const picStream = cfbf.readStream("Pictures");
4683
+ if (!picStream || picStream.length < 32) return urls;
4684
+ const pBuf = new Uint8Array(picStream);
4685
+ const pngSig = [137, 80, 78, 71, 13, 10, 26, 10];
4686
+ const iendSig = [73, 69, 78, 68, 174, 66, 96, 130];
4687
+ for (let i = 0; i <= pBuf.length - 8; i++) {
4688
+ let match = true;
4689
+ for (let j = 0; j < 8; j++) {
4690
+ if (pBuf[i + j] !== pngSig[j]) {
4691
+ match = false;
4692
+ break;
4693
+ }
4694
+ }
4695
+ if (match) {
4696
+ let endIdx = -1;
4697
+ for (let k = i + 8; k <= pBuf.length - 8; k++) {
4698
+ let endMatch = true;
4699
+ for (let j = 0; j < 8; j++) {
4700
+ if (pBuf[k + j] !== iendSig[j]) {
4701
+ endMatch = false;
4702
+ break;
4703
+ }
4704
+ }
4705
+ if (endMatch) {
4706
+ endIdx = k + 8;
4707
+ break;
4708
+ }
4709
+ }
4710
+ if (endIdx !== -1) {
4711
+ const pngBytes = pBuf.subarray(i, endIdx);
4712
+ const blob = new Blob([pngBytes], { type: "image/png" });
4713
+ const url = URL.createObjectURL(blob);
4714
+ urls.push(url);
4715
+ createdUrls.push(url);
4716
+ i = endIdx;
4717
+ }
4718
+ }
4719
+ }
4720
+ for (let i = 0; i <= pBuf.length - 3; i++) {
4721
+ if (pBuf[i] === 255 && pBuf[i + 1] === 216 && pBuf[i + 2] === 255) {
4722
+ let endIdx = -1;
4723
+ for (let k = i + 3; k < pBuf.length - 1; k++) {
4724
+ if (pBuf[k] === 255 && pBuf[k + 1] === 217) {
4725
+ endIdx = k + 2;
4726
+ break;
4727
+ }
4728
+ }
4729
+ if (endIdx !== -1) {
4730
+ const jpgBytes = pBuf.subarray(i, endIdx);
4731
+ const blob = new Blob([jpgBytes], { type: "image/jpeg" });
4732
+ const url = URL.createObjectURL(blob);
4733
+ urls.push(url);
4734
+ createdUrls.push(url);
4735
+ i = endIdx;
4736
+ }
4737
+ }
4738
+ }
4739
+ } catch (err) {
4740
+ console.warn("[PptPlugin] Error extracting pictures:", err);
4741
+ }
4742
+ return urls;
4743
+ }
4525
4744
  /**
4526
4745
  * Traverse PowerPoint binary stream records ([MS-PPT]) and extract text chunks per slide
4527
4746
  */
4528
- extractSlides(stream) {
4747
+ extractSlides(stream, pictures) {
4529
4748
  const view = new DataView(stream.buffer, stream.byteOffset, stream.byteLength);
4530
4749
  const len = stream.length;
4531
4750
  let offset = 0;
4532
4751
  const slides = [];
4533
- let currentSlideTexts = [];
4752
+ let picIdx = 0;
4534
4753
  while (offset + 8 <= len) {
4535
4754
  const recVerInst = view.getUint16(offset, true);
4536
4755
  const recType = view.getUint16(offset + 2, true);
4537
4756
  const recLen = view.getUint32(offset + 4, true);
4757
+ const isContainer = (recVerInst & 15) === 15;
4538
4758
  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 = [];
4759
+ const slideEnd = Math.min(len, offset + 8 + recLen);
4760
+ const rawTexts = [];
4761
+ let hasOle = false;
4762
+ let sOff = offset + 8;
4763
+ while (sOff + 8 <= slideEnd) {
4764
+ const cVerInst = view.getUint16(sOff, true);
4765
+ const cType = view.getUint16(sOff + 2, true);
4766
+ const cLen = view.getUint32(sOff + 4, true);
4767
+ const cIsContainer = (cVerInst & 15) === 15;
4768
+ if ((cType === 4008 || cType === 3998) && cLen > 0 && sOff + 8 + cLen <= slideEnd) {
4769
+ const bytes = stream.subarray(sOff + 8, sOff + 8 + cLen);
4770
+ const txt = new TextDecoder("latin1").decode(bytes).trim();
4771
+ if (txt && !/^[\x00-\x1F]+$/.test(txt) && !txt.includes("[Content_Types]") && !txt.includes("_rels/")) {
4772
+ rawTexts.push(txt);
4773
+ }
4774
+ } else if (cType === 3999 && cLen > 0 && sOff + 8 + cLen <= slideEnd) {
4775
+ const bytes = stream.subarray(sOff + 8, sOff + 8 + cLen);
4776
+ const txt = new TextDecoder("utf-16le").decode(bytes).trim();
4777
+ if (txt && !/^[\x00-\x1F]+$/.test(txt) && !txt.includes("[Content_Types]") && !txt.includes("_rels/")) {
4778
+ rawTexts.push(txt);
4779
+ }
4780
+ } else if (cType === 3009 || cType === 3011) {
4781
+ hasOle = true;
4782
+ }
4783
+ if (cIsContainer) sOff += 8;
4784
+ else sOff += 8 + cLen;
4544
4785
  }
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);
4786
+ let title = "";
4787
+ let subtitle = "";
4788
+ const paragraphs = [];
4789
+ const tableColumns = [];
4790
+ for (const t of rawTexts) {
4791
+ if (!title && t.length < 60 && !t.includes("\n")) {
4792
+ title = t;
4793
+ } else if (t.startsWith("Column ") || title === "Table" && t.startsWith("Column")) {
4794
+ tableColumns.push(t);
4795
+ } else if (t.length < 35 && (t.includes("#") || t.toUpperCase() === t) && !subtitle) {
4796
+ subtitle = t;
4797
+ } else {
4798
+ paragraphs.push(t);
4799
+ }
4553
4800
  }
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);
4801
+ let pictureUrl = null;
4802
+ if ((hasOle || rawTexts.some((t) => t.toLowerCase().includes("chart") || t.toLowerCase().includes("figure"))) && picIdx < pictures.length) {
4803
+ pictureUrl = pictures[picIdx++];
4560
4804
  }
4805
+ slides.push({
4806
+ slideIndex: slides.length + 1,
4807
+ title: title || `Slide ${slides.length + 1}`,
4808
+ subtitle,
4809
+ paragraphs,
4810
+ tableColumns,
4811
+ pictureUrl,
4812
+ hasOle
4813
+ });
4561
4814
  }
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 });
4815
+ if (isContainer) offset += 8;
4816
+ else offset += 8 + recLen;
4573
4817
  }
4574
4818
  if (slides.length === 0) {
4575
4819
  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);
4820
+ const matches = rawText.match(/[A-Za-z0-9\s,.:;!?'"-]{5,}/g) || [];
4821
+ const clean = matches.map((m) => m.trim()).filter(
4822
+ (m) => m.length > 4 && !m.includes("PowerPoint") && !m.includes("Arial") && !m.includes("Times") && !m.includes("[Content_Types]") && !m.includes("_rels/") && !m.includes("xml")
4823
+ );
4824
+ if (clean.length > 0) {
4825
+ const chunkSize = 3;
4826
+ for (let i = 0; i < clean.length; i += chunkSize) {
4827
+ const chunk = clean.slice(i, i + chunkSize);
4582
4828
  slides.push({
4829
+ slideIndex: slides.length + 1,
4583
4830
  title: chunk[0] || `Slide ${Math.floor(i / chunkSize) + 1}`,
4584
- texts: chunk.slice(1)
4831
+ subtitle: chunk.length > 2 ? chunk[1] : void 0,
4832
+ paragraphs: chunk.length > 2 ? chunk.slice(2) : chunk.slice(1),
4833
+ tableColumns: [],
4834
+ pictureUrl: pictures[slides.length] || null
4585
4835
  });
4586
4836
  }
4587
4837
  }