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