@open-file-viewer/core 0.1.14 → 0.1.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -606,6 +606,48 @@ function revokeObjectUrl(url, isExternal) {
606
606
  }
607
607
  }
608
608
 
609
+ // src/messages.ts
610
+ var defaultMessages = {
611
+ "zh-CN": {
612
+ loading: "\u6B63\u5728\u52A0\u8F7D\u9884\u89C8...",
613
+ unsupportedTitle: "\u5F53\u524D\u6587\u4EF6\u6682\u4E0D\u652F\u6301\u5728\u7EBF\u9884\u89C8",
614
+ downloadTitle: "\u5F53\u524D\u6587\u4EF6\u53EF\u4E0B\u8F7D\u540E\u67E5\u770B",
615
+ downloadFile: "\u4E0B\u8F7D\u6587\u4EF6",
616
+ file: "\u6587\u4EF6",
617
+ unnamedFile: "\u672A\u547D\u540D\u6587\u4EF6",
618
+ format: "\u683C\u5F0F",
619
+ unknown: "\u672A\u77E5",
620
+ mime: "MIME",
621
+ undeclared: "\u672A\u58F0\u660E",
622
+ size: "\u5927\u5C0F",
623
+ source: "\u6765\u6E90",
624
+ remoteUrl: "\u8FDC\u7A0B URL",
625
+ localFile: "\u672C\u5730/\u5185\u5B58\u6587\u4EF6"
626
+ },
627
+ "en-US": {
628
+ loading: "Loading preview...",
629
+ unsupportedTitle: "Preview is not available for this file",
630
+ downloadTitle: "This file can be downloaded and opened locally",
631
+ downloadFile: "Download file",
632
+ file: "File",
633
+ unnamedFile: "Untitled file",
634
+ format: "Format",
635
+ unknown: "Unknown",
636
+ mime: "MIME",
637
+ undeclared: "Not declared",
638
+ size: "Size",
639
+ source: "Source",
640
+ remoteUrl: "Remote URL",
641
+ localFile: "Local or in-memory file"
642
+ }
643
+ };
644
+ function resolveMessages(options) {
645
+ return {
646
+ ...defaultMessages[options.locale || "zh-CN"],
647
+ ...options.messages
648
+ };
649
+ }
650
+
609
651
  // src/plugins/fallback.ts
610
652
  function fallbackPlugin() {
611
653
  return {
@@ -623,12 +665,12 @@ function fallbackPlugin() {
623
665
  const panel = document.createElement("div");
624
666
  panel.className = "ofv-fallback";
625
667
  const title = document.createElement("strong");
626
- title.textContent = ctx.options.fallback === "download" ? "\u5F53\u524D\u6587\u4EF6\u53EF\u4E0B\u8F7D\u540E\u67E5\u770B" : "\u5F53\u524D\u6587\u4EF6\u6682\u4E0D\u652F\u6301\u5728\u7EBF\u9884\u89C8";
627
- const meta = createFallbackMeta(ctx.file);
668
+ title.textContent = ctx.options.fallback === "download" ? ctx.options.messages.downloadTitle : ctx.options.messages.unsupportedTitle;
669
+ const meta = createFallbackMeta(ctx.file, ctx.options.messages);
628
670
  const download = document.createElement("a");
629
671
  download.href = url;
630
672
  download.download = ctx.file.name;
631
- download.textContent = "\u4E0B\u8F7D\u6587\u4EF6";
673
+ download.textContent = ctx.options.messages.downloadFile;
632
674
  panel.append(title, meta, download);
633
675
  ctx.viewport.classList.add("ofv-center");
634
676
  ctx.viewport.append(panel);
@@ -644,14 +686,14 @@ function fallbackPlugin() {
644
686
  }
645
687
  };
646
688
  }
647
- function createFallbackMeta(file) {
689
+ function createFallbackMeta(file, messages) {
648
690
  const meta = document.createElement("dl");
649
691
  meta.className = "ofv-fallback-meta";
650
- appendFallbackMeta(meta, "\u6587\u4EF6", file.name || "\u672A\u547D\u540D\u6587\u4EF6");
651
- appendFallbackMeta(meta, "\u683C\u5F0F", file.extension ? `.${file.extension}` : "\u672A\u77E5");
652
- appendFallbackMeta(meta, "MIME", file.mimeType || "\u672A\u58F0\u660E");
653
- appendFallbackMeta(meta, "\u5927\u5C0F", file.size === void 0 ? "\u672A\u77E5" : formatFallbackBytes(file.size));
654
- appendFallbackMeta(meta, "\u6765\u6E90", file.url ? "\u8FDC\u7A0B URL" : "\u672C\u5730/\u5185\u5B58\u6587\u4EF6");
692
+ appendFallbackMeta(meta, messages.file, file.name || messages.unnamedFile);
693
+ appendFallbackMeta(meta, messages.format, file.extension ? `.${file.extension}` : messages.unknown);
694
+ appendFallbackMeta(meta, messages.mime, file.mimeType || messages.undeclared);
695
+ appendFallbackMeta(meta, messages.size, file.size === void 0 ? messages.unknown : formatFallbackBytes(file.size));
696
+ appendFallbackMeta(meta, messages.source, file.url ? messages.remoteUrl : messages.localFile);
655
697
  return meta;
656
698
  }
657
699
  function appendFallbackMeta(parent, label, value) {
@@ -709,15 +751,17 @@ function createViewer(options) {
709
751
  host.append(status, viewport);
710
752
  container.replaceChildren(host);
711
753
  const normalizedOptions = {
754
+ ...options,
712
755
  fit: options.fit || "contain",
713
756
  fallback: options.fallback || "inline",
714
- ...options
757
+ zoom: normalizeInitialZoom(options.zoom),
758
+ messages: resolveMessages(options)
715
759
  };
716
760
  let destroyed = false;
717
761
  let renderToken = 0;
718
762
  const setLoading = (loading) => {
719
763
  status.hidden = !loading;
720
- status.textContent = loading ? "Loading preview..." : "";
764
+ status.textContent = loading ? normalizedOptions.messages.loading : "";
721
765
  };
722
766
  const setError = (error) => {
723
767
  status.hidden = false;
@@ -873,6 +917,9 @@ function clampIndex(index, length) {
873
917
  }
874
918
  return Math.min(Math.max(index, 0), length - 1);
875
919
  }
920
+ function normalizeInitialZoom(zoom) {
921
+ return typeof zoom === "number" && Number.isFinite(zoom) && zoom > 0 ? zoom : 1;
922
+ }
876
923
  function applyTheme(container, theme) {
877
924
  const media = window.matchMedia?.("(prefers-color-scheme: dark)");
878
925
  const classes = ["ofv-theme-light", "ofv-theme-dark"];
@@ -1749,6 +1796,98 @@ async function findPlugin(plugins, file) {
1749
1796
  return fallbackPlugin();
1750
1797
  }
1751
1798
 
1799
+ // src/plugins/utils.ts
1800
+ async function readArrayBuffer(file) {
1801
+ if (file.source instanceof ArrayBuffer) {
1802
+ return file.source;
1803
+ }
1804
+ if (file.blob) {
1805
+ return file.blob.arrayBuffer();
1806
+ }
1807
+ if (typeof file.source === "string") {
1808
+ const response = await fetch(file.source);
1809
+ if (!response.ok) {
1810
+ throw new Error(`Failed to fetch file: ${response.status}`);
1811
+ }
1812
+ return response.arrayBuffer();
1813
+ }
1814
+ throw new Error("Unsupported file source.");
1815
+ }
1816
+ async function readTextFile(file) {
1817
+ const decode = (buffer) => decodeTextBuffer(buffer);
1818
+ if (typeof file.source === "string") {
1819
+ const response = await fetch(file.source);
1820
+ if (!response.ok) {
1821
+ throw new Error(`Failed to fetch text file: ${response.status}`);
1822
+ }
1823
+ return decode(await response.arrayBuffer());
1824
+ }
1825
+ if (file.blob) {
1826
+ return decode(await file.blob.arrayBuffer());
1827
+ }
1828
+ if (file.source instanceof ArrayBuffer) {
1829
+ return decode(file.source);
1830
+ }
1831
+ return String(file.source);
1832
+ }
1833
+ function createPanel(className = "") {
1834
+ const panel = document.createElement("div");
1835
+ panel.className = `ofv-panel ${className}`.trim();
1836
+ return panel;
1837
+ }
1838
+ function getInitialZoom(ctx, min = 0.1, max = 8) {
1839
+ return Math.min(max, Math.max(min, ctx.options.zoom));
1840
+ }
1841
+ function createSection(title) {
1842
+ const section = document.createElement("section");
1843
+ section.className = "ofv-section";
1844
+ const heading = document.createElement("h3");
1845
+ heading.textContent = title;
1846
+ section.append(heading);
1847
+ return section;
1848
+ }
1849
+ function appendMeta(parent, label, value) {
1850
+ const row = document.createElement("div");
1851
+ row.className = "ofv-meta-row";
1852
+ const key = document.createElement("span");
1853
+ key.textContent = label;
1854
+ const content = document.createElement("strong");
1855
+ content.textContent = String(value);
1856
+ row.append(key, content);
1857
+ parent.append(row);
1858
+ }
1859
+ function decodeTextBuffer(buffer) {
1860
+ return decodeTextBytes(new Uint8Array(buffer));
1861
+ }
1862
+ function decodeTextBytes(bytes) {
1863
+ if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) {
1864
+ return new TextDecoder("utf-8").decode(bytes.subarray(3));
1865
+ }
1866
+ if (bytes.length >= 2) {
1867
+ if (bytes[0] === 255 && bytes[1] === 254) {
1868
+ return new TextDecoder("utf-16le").decode(bytes.subarray(2));
1869
+ }
1870
+ if (bytes[0] === 254 && bytes[1] === 255) {
1871
+ return new TextDecoder("utf-16be").decode(bytes.subarray(2));
1872
+ }
1873
+ }
1874
+ try {
1875
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1876
+ } catch {
1877
+ return decodeWithFallback(bytes, "gb18030") || decodeWithFallback(bytes, "gbk") || new TextDecoder("utf-8").decode(bytes);
1878
+ }
1879
+ }
1880
+ function decodeWithFallback(bytes, encoding) {
1881
+ try {
1882
+ return new TextDecoder(encoding).decode(bytes);
1883
+ } catch {
1884
+ return void 0;
1885
+ }
1886
+ }
1887
+ function resolveFormat(file, mimeMap) {
1888
+ return file.extension || mimeMap[file.mimeType] || "";
1889
+ }
1890
+
1752
1891
  // src/plugins/image.ts
1753
1892
  var imageExtensions = /* @__PURE__ */ new Set([
1754
1893
  "jpg",
@@ -1791,12 +1930,12 @@ function imagePlugin() {
1791
1930
  let url = "";
1792
1931
  let convertedBlob = null;
1793
1932
  let isExternal = Boolean(ctx.file.url);
1794
- let canvasSource = null;
1933
+ let tiffSource = null;
1795
1934
  if (isTiff) {
1796
1935
  ctx.setLoading(true);
1797
1936
  try {
1798
1937
  const bytes = await sourceBytesPromise;
1799
- canvasSource = await createTiffCanvas(bytes);
1938
+ tiffSource = await createTiffPreview(bytes, ctx.file.name);
1800
1939
  } catch (err) {
1801
1940
  console.error("TIFF image conversion failed:", err);
1802
1941
  url = createObjectUrl(ctx.file);
@@ -1856,13 +1995,16 @@ function imagePlugin() {
1856
1995
  if (url) {
1857
1996
  image.src = url;
1858
1997
  }
1859
- const visual = canvasSource || image;
1860
- if (canvasSource) {
1861
- canvasSource.classList.add("ofv-media", "ofv-image-content", "ofv-tiff-canvas");
1862
- canvasSource.setAttribute("role", "img");
1863
- canvasSource.setAttribute("aria-label", ctx.file.name);
1998
+ const visual = tiffSource || image;
1999
+ if (tiffSource) {
2000
+ tiffSource.classList.add("ofv-image-content");
2001
+ if (tiffSource.classList.contains("ofv-tiff-pages")) {
2002
+ stage.classList.add("ofv-image-stage-pages");
2003
+ }
1864
2004
  }
1865
- let scale = 1;
2005
+ const visualBox = document.createElement("div");
2006
+ visualBox.className = "ofv-image-scrollbox";
2007
+ let scale = getInitialZoom(ctx);
1866
2008
  let rotation = 0;
1867
2009
  let offsetX = 0;
1868
2010
  let offsetY = 0;
@@ -1875,7 +2017,23 @@ function imagePlugin() {
1875
2017
  let previewAvailable = true;
1876
2018
  const zoomLabel = document.createElement("span");
1877
2019
  zoomLabel.className = "ofv-image-zoom";
2020
+ const updateScrollBox = () => {
2021
+ if (visual.classList.contains("ofv-tiff-pages")) {
2022
+ visualBox.style.removeProperty("width");
2023
+ visualBox.style.removeProperty("height");
2024
+ return;
2025
+ }
2026
+ const baseWidth = visual.offsetWidth || visual.getBoundingClientRect().width || visual.naturalWidth || 0;
2027
+ const baseHeight = visual.offsetHeight || visual.getBoundingClientRect().height || visual.naturalHeight || 0;
2028
+ if (baseWidth > 0) {
2029
+ visualBox.style.width = `${Math.ceil(baseWidth * scale)}px`;
2030
+ }
2031
+ if (baseHeight > 0) {
2032
+ visualBox.style.height = `${Math.ceil(baseHeight * scale)}px`;
2033
+ }
2034
+ };
1878
2035
  const updateTransform = () => {
2036
+ updateScrollBox();
1879
2037
  visual.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale}) rotate(${rotation}deg)`;
1880
2038
  zoomLabel.textContent = `${Math.round(scale * 100)}%`;
1881
2039
  ctx.toolbar?.setZoom(previewAvailable ? scale : void 0);
@@ -1995,11 +2153,13 @@ function imagePlugin() {
1995
2153
  stage.addEventListener("lostpointercapture", onLostPointerCapture);
1996
2154
  stage.addEventListener("pointerleave", onPointerLeave);
1997
2155
  stage.addEventListener("wheel", onWheel, { passive: false });
1998
- if (!canvasSource) {
2156
+ if (!tiffSource) {
1999
2157
  image.addEventListener("error", showImageFallback);
2158
+ image.addEventListener("load", updateScrollBox);
2000
2159
  }
2001
2160
  window.addEventListener("blur", onWindowBlur);
2002
- stage.append(visual);
2161
+ visualBox.append(visual);
2162
+ stage.append(visualBox);
2003
2163
  wrapper.append(...showInlineControls ? [controls, stage, infoBar] : [stage, infoBar]);
2004
2164
  ctx.viewport.append(wrapper);
2005
2165
  updateTransform();
@@ -2037,7 +2197,12 @@ function imagePlugin() {
2037
2197
  },
2038
2198
  resize(size) {
2039
2199
  visual.style.maxWidth = `${size.width}px`;
2040
- visual.style.maxHeight = `${Math.max(0, size.height - controls.offsetHeight)}px`;
2200
+ if (visual.classList.contains("ofv-tiff-pages")) {
2201
+ visual.style.removeProperty("max-height");
2202
+ } else {
2203
+ visual.style.maxHeight = `${Math.max(0, size.height - controls.offsetHeight)}px`;
2204
+ }
2205
+ updateScrollBox();
2041
2206
  },
2042
2207
  destroy() {
2043
2208
  ctx.toolbar?.setZoom(void 0);
@@ -2052,6 +2217,7 @@ function imagePlugin() {
2052
2217
  stage.removeEventListener("pointerleave", onPointerLeave);
2053
2218
  stage.removeEventListener("wheel", onWheel);
2054
2219
  image.removeEventListener("error", showImageFallback);
2220
+ image.removeEventListener("load", updateScrollBox);
2055
2221
  window.removeEventListener("blur", onWindowBlur);
2056
2222
  finishDrag();
2057
2223
  wrapper.remove();
@@ -2065,33 +2231,64 @@ function imagePlugin() {
2065
2231
  }
2066
2232
  };
2067
2233
  }
2068
- async function createTiffCanvas(bytes) {
2234
+ async function createTiffPreview(bytes, fileName) {
2069
2235
  if (bytes.byteLength === 0) {
2070
2236
  throw new Error("\u65E0\u6CD5\u8BFB\u53D6 TIFF \u6587\u4EF6\u5185\u5BB9\u3002");
2071
2237
  }
2072
2238
  const UTIF = await import("utif");
2073
2239
  const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
2074
2240
  const ifds = UTIF.decode(buffer);
2075
- const ifd = ifds.find((item) => Number(item.width || 0) > 0 || Number(item.t256 || 0) > 0) || ifds[0];
2076
- if (!ifd) {
2241
+ const imageIfds = ifds.filter((item) => hasTiffDimensions(item));
2242
+ if (imageIfds.length === 0) {
2077
2243
  throw new Error("TIFF \u6587\u4EF6\u6CA1\u6709\u53EF\u89E3\u7801\u7684\u56FE\u50CF\u76EE\u5F55\u3002");
2078
2244
  }
2245
+ const pages = imageIfds.map((ifd, index) => createTiffPage(UTIF, buffer, ifd, index, imageIfds.length, fileName));
2246
+ if (pages.length === 1) {
2247
+ return pages[0].canvas;
2248
+ }
2249
+ const container = document.createElement("div");
2250
+ container.className = "ofv-tiff-pages";
2251
+ container.setAttribute("role", "group");
2252
+ container.setAttribute("aria-label", `${fileName}\uFF0C\u5171 ${pages.length} \u9875`);
2253
+ for (const page of pages) {
2254
+ const figure = document.createElement("figure");
2255
+ figure.className = "ofv-tiff-page";
2256
+ const caption = document.createElement("figcaption");
2257
+ caption.textContent = `\u7B2C ${page.index + 1} / ${pages.length} \u9875 \xB7 ${page.width} x ${page.height}px`;
2258
+ figure.append(page.canvas, caption);
2259
+ container.append(figure);
2260
+ }
2261
+ return container;
2262
+ }
2263
+ function createTiffPage(UTIF, buffer, ifd, index, total, fileName) {
2079
2264
  UTIF.decodeImage(buffer, ifd);
2080
2265
  const rgba = UTIF.toRGBA8(ifd);
2081
- const width = Number(ifd.width || ifd.t256 || 0);
2082
- const height = Number(ifd.height || ifd.t257 || 0);
2266
+ const { width, height } = getTiffDimensions(ifd);
2083
2267
  if (!width || !height || rgba.length < width * height * 4) {
2084
2268
  throw new Error("TIFF \u56FE\u50CF\u50CF\u7D20\u6570\u636E\u4E0D\u5B8C\u6574\u3002");
2085
2269
  }
2086
2270
  const canvas = document.createElement("canvas");
2271
+ canvas.className = "ofv-tiff-canvas";
2087
2272
  canvas.width = width;
2088
2273
  canvas.height = height;
2274
+ canvas.setAttribute("role", "img");
2275
+ canvas.setAttribute("aria-label", total > 1 ? `${fileName} \u7B2C ${index + 1} \u9875` : fileName);
2089
2276
  const context = canvas.getContext("2d");
2090
2277
  if (!context) {
2091
2278
  throw new Error("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301 Canvas 2D\uFF0C\u65E0\u6CD5\u5C55\u793A TIFF\u3002");
2092
2279
  }
2093
2280
  context.putImageData(new ImageData(new Uint8ClampedArray(rgba), width, height), 0, 0);
2094
- return canvas;
2281
+ return { canvas, width, height, index };
2282
+ }
2283
+ function hasTiffDimensions(ifd) {
2284
+ const { width, height } = getTiffDimensions(ifd);
2285
+ return width > 0 && height > 0;
2286
+ }
2287
+ function getTiffDimensions(ifd) {
2288
+ return {
2289
+ width: Number(ifd.width || ifd.t256 || 0),
2290
+ height: Number(ifd.height || ifd.t257 || 0)
2291
+ };
2095
2292
  }
2096
2293
  function createImageFallback(fileName, url) {
2097
2294
  const fallback = document.createElement("div");
@@ -2420,6 +2617,7 @@ function parseTiffInfo(bytes) {
2420
2617
  return { format: magic === 43 ? "BigTIFF" : "TIFF", note: "IFD \u504F\u79FB\u8D85\u51FA\u6587\u4EF6\u8303\u56F4" };
2421
2618
  }
2422
2619
  const count = magic === 43 ? readTiffUint64AsNumber(bytes, ifdOffset, littleEndian) : readTiffUint16(bytes, ifdOffset, littleEndian);
2620
+ const imageCount = countTiffIfds(bytes, ifdOffset, magic === 43, littleEndian);
2423
2621
  const entrySize = magic === 43 ? 20 : 12;
2424
2622
  const entriesStart = ifdOffset + (magic === 43 ? 8 : 2);
2425
2623
  let width;
@@ -2446,9 +2644,26 @@ function parseTiffInfo(bytes) {
2446
2644
  height,
2447
2645
  bitDepth: bitDepth ? `${bitDepth} bit` : void 0,
2448
2646
  color: compression ? tiffCompressionName(compression) : void 0,
2449
- count: Number.isFinite(count) ? count : void 0
2647
+ count: imageCount || void 0
2450
2648
  };
2451
2649
  }
2650
+ function countTiffIfds(bytes, firstOffset, bigTiff, littleEndian) {
2651
+ const seen = /* @__PURE__ */ new Set();
2652
+ let offset = firstOffset;
2653
+ let count = 0;
2654
+ while (Number.isFinite(offset) && offset > 0 && offset < bytes.length && !seen.has(offset) && count < 512) {
2655
+ seen.add(offset);
2656
+ const entryCount = bigTiff ? readTiffUint64AsNumber(bytes, offset, littleEndian) : readTiffUint16(bytes, offset, littleEndian);
2657
+ const entrySize = bigTiff ? 20 : 12;
2658
+ const nextOffsetPosition = offset + (bigTiff ? 8 : 2) + entryCount * entrySize;
2659
+ if (!Number.isFinite(entryCount) || nextOffsetPosition + (bigTiff ? 8 : 4) > bytes.length) {
2660
+ break;
2661
+ }
2662
+ count++;
2663
+ offset = bigTiff ? readTiffUint64AsNumber(bytes, nextOffsetPosition, littleEndian) : readTiffUint32(bytes, nextOffsetPosition, littleEndian);
2664
+ }
2665
+ return count;
2666
+ }
2452
2667
  function readTiffInlineValue(bytes, offset, type, littleEndian) {
2453
2668
  if (offset + 4 > bytes.length) {
2454
2669
  return void 0;
@@ -3690,95 +3905,6 @@ function readAiffExtended(bytes, offset) {
3690
3905
  return mantissa * Math.pow(2, exponent - 16383 - 63);
3691
3906
  }
3692
3907
 
3693
- // src/plugins/utils.ts
3694
- async function readArrayBuffer(file) {
3695
- if (file.source instanceof ArrayBuffer) {
3696
- return file.source;
3697
- }
3698
- if (file.blob) {
3699
- return file.blob.arrayBuffer();
3700
- }
3701
- if (typeof file.source === "string") {
3702
- const response = await fetch(file.source);
3703
- if (!response.ok) {
3704
- throw new Error(`Failed to fetch file: ${response.status}`);
3705
- }
3706
- return response.arrayBuffer();
3707
- }
3708
- throw new Error("Unsupported file source.");
3709
- }
3710
- async function readTextFile(file) {
3711
- const decode = (buffer) => decodeTextBuffer(buffer);
3712
- if (typeof file.source === "string") {
3713
- const response = await fetch(file.source);
3714
- if (!response.ok) {
3715
- throw new Error(`Failed to fetch text file: ${response.status}`);
3716
- }
3717
- return decode(await response.arrayBuffer());
3718
- }
3719
- if (file.blob) {
3720
- return decode(await file.blob.arrayBuffer());
3721
- }
3722
- if (file.source instanceof ArrayBuffer) {
3723
- return decode(file.source);
3724
- }
3725
- return String(file.source);
3726
- }
3727
- function createPanel(className = "") {
3728
- const panel = document.createElement("div");
3729
- panel.className = `ofv-panel ${className}`.trim();
3730
- return panel;
3731
- }
3732
- function createSection(title) {
3733
- const section = document.createElement("section");
3734
- section.className = "ofv-section";
3735
- const heading = document.createElement("h3");
3736
- heading.textContent = title;
3737
- section.append(heading);
3738
- return section;
3739
- }
3740
- function appendMeta(parent, label, value) {
3741
- const row = document.createElement("div");
3742
- row.className = "ofv-meta-row";
3743
- const key = document.createElement("span");
3744
- key.textContent = label;
3745
- const content = document.createElement("strong");
3746
- content.textContent = String(value);
3747
- row.append(key, content);
3748
- parent.append(row);
3749
- }
3750
- function decodeTextBuffer(buffer) {
3751
- return decodeTextBytes(new Uint8Array(buffer));
3752
- }
3753
- function decodeTextBytes(bytes) {
3754
- if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) {
3755
- return new TextDecoder("utf-8").decode(bytes.subarray(3));
3756
- }
3757
- if (bytes.length >= 2) {
3758
- if (bytes[0] === 255 && bytes[1] === 254) {
3759
- return new TextDecoder("utf-16le").decode(bytes.subarray(2));
3760
- }
3761
- if (bytes[0] === 254 && bytes[1] === 255) {
3762
- return new TextDecoder("utf-16be").decode(bytes.subarray(2));
3763
- }
3764
- }
3765
- try {
3766
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
3767
- } catch {
3768
- return decodeWithFallback(bytes, "gb18030") || decodeWithFallback(bytes, "gbk") || new TextDecoder("utf-8").decode(bytes);
3769
- }
3770
- }
3771
- function decodeWithFallback(bytes, encoding) {
3772
- try {
3773
- return new TextDecoder(encoding).decode(bytes);
3774
- } catch {
3775
- return void 0;
3776
- }
3777
- }
3778
- function resolveFormat(file, mimeMap) {
3779
- return file.extension || mimeMap[file.mimeType] || "";
3780
- }
3781
-
3782
3908
  // src/plugins/text.ts
3783
3909
  var langMap = {
3784
3910
  js: "javascript",
@@ -3936,26 +4062,6 @@ var mimeLangMap = {
3936
4062
  };
3937
4063
  var MAX_HIGHLIGHT_CHARS = 18e4;
3938
4064
  var MAX_RENDER_CHARS = 6e5;
3939
- function loadPrismCss(theme) {
3940
- const lightId = "ofv-prism-css-light";
3941
- const darkId = "ofv-prism-css-dark";
3942
- const activeId = theme === "dark" ? darkId : lightId;
3943
- const inactiveId = theme === "dark" ? lightId : darkId;
3944
- document.getElementById(inactiveId)?.remove();
3945
- if (document.getElementById(activeId)) {
3946
- return Promise.resolve();
3947
- }
3948
- const href = theme === "dark" ? "https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css" : "https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism.min.css";
3949
- return new Promise((resolve, reject) => {
3950
- const link = document.createElement("link");
3951
- link.id = activeId;
3952
- link.rel = "stylesheet";
3953
- link.href = href;
3954
- link.onload = () => resolve();
3955
- link.onerror = () => reject(new Error(`Failed to load Prism CSS: ${href}`));
3956
- document.head.appendChild(link);
3957
- });
3958
- }
3959
4065
  function textPlugin() {
3960
4066
  return {
3961
4067
  name: "text",
@@ -3979,7 +4085,6 @@ function textPlugin() {
3979
4085
  }
3980
4086
  };
3981
4087
  }
3982
- const isDark = ctx.host.parentElement?.classList.contains("ofv-theme-dark") || document.body.classList.contains("ofv-theme-dark") || ctx.options.theme === "auto" && window.matchMedia?.("(prefers-color-scheme: dark)").matches || ctx.options.theme === "dark";
3983
4088
  if (isMarkdown) {
3984
4089
  const [markedModule, PrismModule2, DOMPurifyModule] = await Promise.all([
3985
4090
  import("marked"),
@@ -4000,7 +4105,6 @@ function textPlugin() {
4000
4105
  try {
4001
4106
  const codeBlocks = container.querySelectorAll("pre code");
4002
4107
  if (codeBlocks.length > 0) {
4003
- await loadPrismCss(isDark ? "dark" : "light");
4004
4108
  codeBlocks.forEach((block) => {
4005
4109
  const parent = block.parentElement;
4006
4110
  if (parent && !parent.className.includes("language-")) {
@@ -4124,9 +4228,6 @@ function textPlugin() {
4124
4228
  console.warn(`Prism failed to load language component for: ${lang}`, e);
4125
4229
  }
4126
4230
  }
4127
- await loadPrismCss(isDark ? "dark" : "light").catch((error) => {
4128
- console.warn("Prism CSS failed to load; rendering code without external theme:", error);
4129
- });
4130
4231
  const codeText = text.length > MAX_RENDER_CHARS ? text.slice(0, MAX_RENDER_CHARS) : text;
4131
4232
  const totalLines = countLines(text);
4132
4233
  const shownLines = countLines(codeText);
@@ -4247,7 +4348,7 @@ function textPlugin() {
4247
4348
  };
4248
4349
  }
4249
4350
  function createTextZoomController(target, cssVariable, ctx) {
4250
- let zoom = 1;
4351
+ let zoom = getInitialZoom(ctx, 0.5, 3);
4251
4352
  const apply = () => {
4252
4353
  const normalized = Math.round(zoom * 100) / 100;
4253
4354
  target.style.setProperty(cssVariable, String(normalized));
@@ -4557,6 +4658,7 @@ function pdfPlugin(options = {}) {
4557
4658
  viewport: ctx.viewport,
4558
4659
  size: ctx.size,
4559
4660
  fit: ctx.options.fit,
4661
+ zoom: ctx.options.zoom,
4560
4662
  toolbar: ctx.toolbar,
4561
4663
  encryptedTitle: "PDF \u5DF2\u52A0\u5BC6\uFF0C\u65E0\u6CD5\u5728\u7EBF\u9884\u89C8",
4562
4664
  encryptedMessage: "\u8BF7\u4E0B\u8F7D\u540E\u4F7F\u7528\u5BC6\u7801\u6253\u5F00\uFF0C\u6216\u4E0A\u4F20\u89E3\u5BC6\u540E\u7684 PDF \u6587\u4EF6\u3002",
@@ -4653,16 +4755,17 @@ async function renderPdfDocumentPreview(options) {
4653
4755
  const baseViewport = page.getViewport({ scale: 1 });
4654
4756
  pagesMeta.push({
4655
4757
  width: baseViewport.width,
4656
- height: baseViewport.height
4758
+ height: baseViewport.height,
4759
+ rotation: getPdfPageRotation(page)
4657
4760
  });
4658
4761
  } catch {
4659
- pagesMeta.push({ width: 612, height: 792 });
4762
+ pagesMeta.push({ width: 612, height: 792, rotation: 0 });
4660
4763
  }
4661
4764
  }
4662
4765
  const pageStates = [];
4663
4766
  let observer = null;
4664
4767
  let currentSize = options.size;
4665
- let zoomFactor = 1;
4768
+ let zoomFactor = getInitialZoom({ options: { zoom: options.zoom ?? 1 } }, 0.25, 4);
4666
4769
  let rotation = 0;
4667
4770
  const updateSummary = () => {
4668
4771
  renderPdfSummary(summary, pdfDocument.numPages, pagesMeta, options.fit, zoomFactor);
@@ -4691,7 +4794,7 @@ async function renderPdfDocumentPreview(options) {
4691
4794
  const page = await pdfDocument.getPage(pageIdx + 1);
4692
4795
  const meta = pagesMeta[pageIdx];
4693
4796
  const scale = options.fit === "actual" ? zoomFactor : Math.max(0.05, Math.min(5, getPdfAvailableWidth(size.width) / rotatedPdfWidth(meta, rotation) * zoomFactor));
4694
- const viewport = page.getViewport({ scale, rotation });
4797
+ const viewport = page.getViewport({ scale, rotation: getPdfRenderRotation(meta, rotation) });
4695
4798
  const outputScale = getPdfOutputScale();
4696
4799
  const cssWidth = Math.floor(viewport.width);
4697
4800
  const cssHeight = Math.floor(viewport.height);
@@ -4730,6 +4833,8 @@ async function renderPdfDocumentPreview(options) {
4730
4833
  const span = document.createElement("span");
4731
4834
  span.textContent = str;
4732
4835
  span.style.fontSize = `${fontHeight}px`;
4836
+ span.style.lineHeight = "1";
4837
+ span.style.height = `${fontHeight}px`;
4733
4838
  span.style.fontFamily = item.fontName || "sans-serif";
4734
4839
  span.style.left = `${tx[4]}px`;
4735
4840
  span.style.top = `${tx[5] - fontHeight}px`;
@@ -4927,6 +5032,13 @@ function appendPdfSummary(parent, label, value) {
4927
5032
  function normalizePdfRotation(value) {
4928
5033
  return (value % 360 + 360) % 360;
4929
5034
  }
5035
+ function getPdfPageRotation(page) {
5036
+ const rotation = Number(page.rotate);
5037
+ return Number.isFinite(rotation) ? normalizePdfRotation(rotation) : 0;
5038
+ }
5039
+ function getPdfRenderRotation(meta, userRotation) {
5040
+ return normalizePdfRotation((meta.rotation || 0) + userRotation);
5041
+ }
4930
5042
  function isPdfRotatedSideways(rotation) {
4931
5043
  const normalized = normalizePdfRotation(rotation);
4932
5044
  return normalized === 90 || normalized === 270;
@@ -5897,13 +6009,13 @@ function officePlugin(options = {}) {
5897
6009
  if (conversionContext && await shouldUseOfficeConversion(options, conversionContext)) {
5898
6010
  delegatedInstance = await renderConvertedOfficePreview(panel, ctx, options, conversionContext);
5899
6011
  } else if (packageFormat === "docx" && !fileIsDocx(extension)) {
5900
- disposeDocxFit = await renderDocx(panel, arrayBuffer);
6012
+ disposeDocxFit = await renderDocx(panel, arrayBuffer, ctx.options.fit);
5901
6013
  } else if (packageFormat === "xlsx" && !sheetExtensions.has(extension)) {
5902
6014
  await renderSheet(panel, arrayBuffer, "xlsx");
5903
6015
  } else if (packageFormat === "pptx" && !["pptx", "pptm", "ppsx", "ppsm", "potx", "potm"].includes(extension)) {
5904
6016
  await renderPptx(panel, arrayBuffer);
5905
6017
  } else if (fileIsDocx(extension)) {
5906
- disposeDocxFit = await renderDocx(panel, arrayBuffer);
6018
+ disposeDocxFit = await renderDocx(panel, arrayBuffer, ctx.options.fit);
5907
6019
  } else if (extension === "rtf") {
5908
6020
  renderPlainDocument(panel, "RTF \u6587\u6863", rtfToText(await readTextFromBuffer(arrayBuffer)));
5909
6021
  } else if (extension === "odt") {
@@ -5912,7 +6024,7 @@ function officePlugin(options = {}) {
5912
6024
  renderOpenDocumentXml(panel, "FODT \u6587\u6863", await readTextFromBuffer(arrayBuffer));
5913
6025
  } else if (extension === "fods") {
5914
6026
  renderFlatOds(panel, await readTextFromBuffer(arrayBuffer));
5915
- } else if (packagedOfficeCandidates.has(extension) && await renderPackagedOfficePreview(panel, arrayBuffer, extension)) {
6027
+ } else if (packagedOfficeCandidates.has(extension) && await renderPackagedOfficePreview(panel, arrayBuffer, extension, ctx.options.fit)) {
5916
6028
  } else if (sheetExtensions.has(extension)) {
5917
6029
  await renderSheet(panel, arrayBuffer, extension);
5918
6030
  } else if (["pptx", "pptm", "ppsx", "ppsm", "potx", "potm"].includes(extension)) {
@@ -5987,6 +6099,7 @@ async function renderConvertedOfficePreview(panel, ctx, options, conversionConte
5987
6099
  viewport: panel,
5988
6100
  size: ctx.size,
5989
6101
  fit: ctx.options.fit,
6102
+ zoom: ctx.options.zoom,
5990
6103
  toolbar: ctx.toolbar,
5991
6104
  title: "Office \u9AD8\u4FDD\u771F\u8F6C\u6362\u9884\u89C8",
5992
6105
  fallbackTitle: "Office \u8F6C\u6362\u540E\u7684 PDF \u65E0\u6CD5\u9884\u89C8",
@@ -6050,7 +6163,7 @@ function createOfficeZoomController(panel, ctx) {
6050
6163
  if (!canZoom) {
6051
6164
  return void 0;
6052
6165
  }
6053
- let zoom = 1;
6166
+ let zoom = getInitialZoom(ctx, 0.5, 3);
6054
6167
  const apply = () => {
6055
6168
  panel.style.setProperty("--ofv-office-zoom", String(zoom));
6056
6169
  panel.dispatchEvent(new CustomEvent("ofv-office-zoom"));
@@ -6113,7 +6226,7 @@ async function detectPackagedOfficeFormat(arrayBuffer) {
6113
6226
  }
6114
6227
  return void 0;
6115
6228
  }
6116
- async function renderDocx(panel, arrayBuffer) {
6229
+ async function renderDocx(panel, arrayBuffer, fit) {
6117
6230
  const content = document.createElement("div");
6118
6231
  content.className = "ofv-docx-document";
6119
6232
  const styleContainer = document.createElement("div");
@@ -6152,7 +6265,7 @@ async function renderDocx(panel, arrayBuffer) {
6152
6265
  return () => void 0;
6153
6266
  }
6154
6267
  panel.append(content);
6155
- disposeFit = fitDocxPages(content);
6268
+ disposeFit = fitDocxPages(content, fit);
6156
6269
  return () => {
6157
6270
  disposeFit?.();
6158
6271
  styleContainer.remove();
@@ -6750,7 +6863,8 @@ function looksLikeDocxTextboxHeading(value) {
6750
6863
  return text.length > 0 && text.length <= 12 && !/[0-9@.::]/.test(text);
6751
6864
  }
6752
6865
  async function normalizeDocxLayout(container, arrayBuffer) {
6753
- const hints = await readDocxLayoutHints(arrayBuffer);
6866
+ const [hints, charts] = await Promise.all([readDocxLayoutHints(arrayBuffer), readDocxCharts(arrayBuffer)]);
6867
+ repairDocxChartPlaceholders(container, charts);
6754
6868
  const pages = container.querySelectorAll("section.ofv-docx");
6755
6869
  for (const page of pages) {
6756
6870
  repairDocxShapeFills(page);
@@ -6765,6 +6879,75 @@ async function normalizeDocxLayout(container, arrayBuffer) {
6765
6879
  }
6766
6880
  }
6767
6881
  }
6882
+ async function readDocxCharts(arrayBuffer) {
6883
+ try {
6884
+ const zip = await import_jszip3.default.loadAsync(arrayBuffer);
6885
+ const documentXml = await zip.file("word/document.xml")?.async("text");
6886
+ const documentDoc = documentXml ? parseOfficeXml(documentXml) : void 0;
6887
+ if (!documentDoc) {
6888
+ return [];
6889
+ }
6890
+ const relationships = await readOfficeRelationships(zip, "word/document.xml");
6891
+ const chartDrawings = Array.from(documentDoc.getElementsByTagName("*")).filter((element) => element.localName === "inline" || element.localName === "anchor").map((element) => readDocxChartDrawing(element)).filter((item) => Boolean(item?.relationshipId));
6892
+ const charts = [];
6893
+ for (const [index, drawing] of chartDrawings.entries()) {
6894
+ const chartRel = relationships.find((rel) => rel.id === drawing.relationshipId && /\/chart$/i.test(rel.type));
6895
+ const chartPath = resolveOfficeRelationshipTarget("word/document.xml", chartRel?.target);
6896
+ const chartXml = chartPath ? await zip.file(chartPath)?.async("text") : void 0;
6897
+ const chart = chartXml ? parseChartXml(chartXml, chartPath?.split("/").pop() || `chart${index + 1}.xml`) : null;
6898
+ if (chart) {
6899
+ charts.push({ ...chart, widthPt: drawing.widthPt, heightPt: drawing.heightPt });
6900
+ }
6901
+ }
6902
+ return charts;
6903
+ } catch {
6904
+ return [];
6905
+ }
6906
+ }
6907
+ function readDocxChartDrawing(element) {
6908
+ const chart = Array.from(element.getElementsByTagName("*")).find((child) => child.localName === "chart");
6909
+ const relationshipId = chart ? getXmlAttribute3(chart, "id") : "";
6910
+ if (!relationshipId) {
6911
+ return void 0;
6912
+ }
6913
+ const extent = Array.from(element.children).find((child) => child.localName === "extent");
6914
+ return {
6915
+ relationshipId,
6916
+ widthPt: emuToPt(Number(extent?.getAttribute("cx") || 0)),
6917
+ heightPt: emuToPt(Number(extent?.getAttribute("cy") || 0))
6918
+ };
6919
+ }
6920
+ function repairDocxChartPlaceholders(container, charts) {
6921
+ if (charts.length === 0) {
6922
+ return;
6923
+ }
6924
+ const placeholders = Array.from(container.querySelectorAll("section.ofv-docx div")).filter(isDocxChartPlaceholder);
6925
+ if (placeholders.length !== charts.length) {
6926
+ return;
6927
+ }
6928
+ placeholders.forEach((placeholder, index) => {
6929
+ const chart = charts[index];
6930
+ placeholder.classList.add("ofv-docx-chart-preview");
6931
+ placeholder.dataset.ofvDocxChartPreview = "true";
6932
+ if (chart.widthPt > 0) {
6933
+ placeholder.style.width ||= `${formatCssNumber(chart.widthPt)}pt`;
6934
+ }
6935
+ if (chart.heightPt > 0) {
6936
+ placeholder.style.height ||= `${formatCssNumber(chart.heightPt)}pt`;
6937
+ }
6938
+ placeholder.replaceChildren(renderChartSvg(chart));
6939
+ });
6940
+ }
6941
+ function isDocxChartPlaceholder(element) {
6942
+ if (element.dataset.ofvDocxChartPreview === "true" || element.children.length > 0 || normalizePreviewText(element.textContent || "")) {
6943
+ return false;
6944
+ }
6945
+ const display = element.style.display;
6946
+ const position = element.style.position;
6947
+ const width = parseCssPixelValue(element.style.width);
6948
+ const height = parseCssPixelValue(element.style.height);
6949
+ return display === "inline-block" && position === "relative" && width >= 120 && height >= 80;
6950
+ }
6768
6951
  function repairDocxHeadingShapeAlignment(page) {
6769
6952
  for (const paragraph of page.querySelectorAll("p")) {
6770
6953
  const text = normalizePreviewText(paragraph.textContent || "");
@@ -6915,14 +7098,19 @@ function repairDocxShapeFills(page) {
6915
7098
  }
6916
7099
  }
6917
7100
  function repairDocxFloatingPictures(page, hints) {
6918
- const hint = hints.floatingPictures.find((item) => item.relativeFrom === "column" && item.wrap === "square");
6919
- if (!hint) {
7101
+ const pageHints = hints.floatingPictures.filter((item) => item.relativeFrom === "column" && item.wrap === "square");
7102
+ if (pageHints.length === 0) {
6920
7103
  return;
6921
7104
  }
6922
- const image = page.querySelector("img");
6923
- if (!image) {
7105
+ const images = Array.from(page.querySelectorAll("img")).filter(
7106
+ (image) => image.closest("[data-ofv-docx-float-repaired='true']") === null
7107
+ );
7108
+ if (images.length === 0 || images.length !== pageHints.length) {
6924
7109
  return;
6925
7110
  }
7111
+ images.forEach((image, index) => repairDocxFloatingPicture(page, image, pageHints[index]));
7112
+ }
7113
+ function repairDocxFloatingPicture(page, image, hint) {
6926
7114
  const wrapper = image.parentElement;
6927
7115
  if (!wrapper || wrapper.dataset.ofvDocxFloatRepaired === "true") {
6928
7116
  return;
@@ -6968,7 +7156,7 @@ function parseCssLineHeight(value) {
6968
7156
  const parsed = Number.parseFloat(trimmed);
6969
7157
  return Number.isFinite(parsed) ? parsed : 0;
6970
7158
  }
6971
- function fitDocxPages(container) {
7159
+ function fitDocxPages(container, fit) {
6972
7160
  const wrapper = container.querySelector(".ofv-docx-wrapper");
6973
7161
  if (!wrapper) {
6974
7162
  return () => void 0;
@@ -6981,6 +7169,7 @@ function fitDocxPages(container) {
6981
7169
  return;
6982
7170
  }
6983
7171
  const availableWidth = Math.max(1, container.clientWidth - 48);
7172
+ const availableHeight = container.clientHeight > 0 ? Math.max(1, container.clientHeight - 48) : void 0;
6984
7173
  const pageWidth = Math.max(
6985
7174
  1,
6986
7175
  ...frames.map(({ page }) => {
@@ -6988,14 +7177,24 @@ function fitDocxPages(container) {
6988
7177
  return page.offsetWidth || rectWidth || parseCssPixelValue(page.style.width) || 794;
6989
7178
  })
6990
7179
  );
6991
- const scale = Math.min(1, Math.max(0.35, availableWidth / pageWidth));
7180
+ const pageHeight = Math.max(
7181
+ 1,
7182
+ ...frames.map(({ page }) => page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height) || 1123)
7183
+ );
7184
+ const scale = getDocxFitScale(fit, availableWidth, availableHeight, pageWidth, pageHeight);
6992
7185
  const userZoom = parseCssPixelValue(panel?.style.getPropertyValue("--ofv-office-zoom") || "1") || 1;
6993
7186
  wrapper.style.setProperty("--ofv-docx-scale", formatCssNumber(scale));
6994
7187
  wrapper.style.setProperty("--ofv-docx-page-width", `${pageWidth}px`);
7188
+ wrapper.style.width = `${Math.ceil(pageWidth * scale * userZoom + 48)}px`;
7189
+ wrapper.style.maxWidth = "none";
7190
+ wrapper.style.overflow = "visible";
6995
7191
  for (const { frame, page } of frames) {
6996
- const pageHeight = page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height);
6997
- if (pageHeight > 0) {
6998
- frame.style.height = `${Math.ceil(pageHeight * scale * userZoom)}px`;
7192
+ const framePageHeight = page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height);
7193
+ const framePageWidth = page.offsetWidth || page.getBoundingClientRect().width || parseCssPixelValue(page.style.width) || pageWidth;
7194
+ frame.style.width = `${Math.ceil(framePageWidth * scale * userZoom)}px`;
7195
+ frame.style.maxWidth = "none";
7196
+ if (framePageHeight > 0) {
7197
+ frame.style.height = `${Math.ceil(framePageHeight * scale * userZoom)}px`;
6999
7198
  }
7000
7199
  }
7001
7200
  };
@@ -7020,6 +7219,26 @@ function fitDocxPages(container) {
7020
7219
  observer.disconnect();
7021
7220
  };
7022
7221
  }
7222
+ function getDocxFitScale(fit, availableWidth, availableHeight, pageWidth, pageHeight) {
7223
+ const widthScale = availableWidth / pageWidth;
7224
+ const heightScale = availableHeight ? availableHeight / pageHeight : void 0;
7225
+ if (fit === "actual") {
7226
+ return 1;
7227
+ }
7228
+ if (fit === "height") {
7229
+ return Math.max(0.1, heightScale ?? widthScale);
7230
+ }
7231
+ if (fit === "cover") {
7232
+ return Math.max(0.1, Math.max(widthScale, heightScale ?? widthScale));
7233
+ }
7234
+ if (fit === "scale-down") {
7235
+ return Math.min(1, Math.max(0.1, Math.min(widthScale, heightScale ?? widthScale)));
7236
+ }
7237
+ if (fit === "contain") {
7238
+ return Math.max(0.1, Math.min(widthScale, heightScale ?? widthScale));
7239
+ }
7240
+ return Math.max(0.1, widthScale);
7241
+ }
7023
7242
  function ensureDocxPageFrames(wrapper) {
7024
7243
  const pages = Array.from(wrapper.querySelectorAll("section.ofv-docx"));
7025
7244
  return pages.map((page) => {
@@ -7551,9 +7770,9 @@ function parseChartXml(xml, fallbackName) {
7551
7770
  return null;
7552
7771
  }
7553
7772
  const type = detectChartType(doc);
7554
- const title = textFromFirst(doc, "title") || fallbackName.replace(/\.xml$/i, "");
7773
+ const title = readChartTitle(doc, fallbackName);
7555
7774
  const seriesElements = Array.from(doc.getElementsByTagName("*")).filter((element) => element.localName === "ser");
7556
- const series = seriesElements.map((element, index) => parseChartSeries(element, index)).filter((item) => item.values.length > 0);
7775
+ const series = seriesElements.map((element, index) => ({ ...parseChartSeries(element, index), color: readChartSeriesColor(element) })).filter((item) => item.values.length > 0);
7557
7776
  if (series.length === 0) {
7558
7777
  return null;
7559
7778
  }
@@ -7562,9 +7781,21 @@ function parseChartXml(xml, fallbackName) {
7562
7781
  type,
7563
7782
  title,
7564
7783
  categories: series.find((item) => item.categories.length > 0)?.categories || [],
7565
- series: series.map((item) => ({ name: item.name, values: item.values }))
7784
+ series: series.map((item) => ({ name: item.name, values: item.values, color: item.color }))
7566
7785
  };
7567
7786
  }
7787
+ function readChartTitle(doc, fallbackName) {
7788
+ const titleElement = Array.from(doc.getElementsByTagName("*")).find((element) => element.localName === "title");
7789
+ if (!titleElement) {
7790
+ return fallbackName.replace(/\.xml$/i, "");
7791
+ }
7792
+ const explicitTitle = chartText(titleElement);
7793
+ if (explicitTitle) {
7794
+ return explicitTitle;
7795
+ }
7796
+ const language = Array.from(doc.getElementsByTagName("*")).find((element) => element.localName === "lang")?.getAttribute("val") || "";
7797
+ return /^zh\b/i.test(language) ? "\u56FE\u8868\u6807\u9898" : "Chart Title";
7798
+ }
7568
7799
  function detectChartType(doc) {
7569
7800
  const chartType = Array.from(doc.getElementsByTagName("*")).find(
7570
7801
  (element) => element.localName.endsWith("Chart") && element.localName !== "chart"
@@ -7581,6 +7812,31 @@ function parseChartSeries(element, index) {
7581
7812
  categories: stringsFromFirst(element, "cat")
7582
7813
  };
7583
7814
  }
7815
+ function readChartSeriesColor(element) {
7816
+ const shape = Array.from(element?.children || []).find((child) => child.localName === "spPr");
7817
+ const color = Array.from(shape?.getElementsByTagName("*") || []).find(
7818
+ (child) => child.localName === "srgbClr" || child.localName === "schemeClr"
7819
+ );
7820
+ if (!color) {
7821
+ return void 0;
7822
+ }
7823
+ if (color.localName === "srgbClr") {
7824
+ const value = color.getAttribute("val") || "";
7825
+ return /^[\da-f]{6}$/i.test(value) ? `#${value}` : void 0;
7826
+ }
7827
+ return chartSchemeColor(color.getAttribute("val") || "");
7828
+ }
7829
+ function chartSchemeColor(value) {
7830
+ const colors = {
7831
+ accent1: "#156082",
7832
+ accent2: "#e97132",
7833
+ accent3: "#196b24",
7834
+ accent4: "#0f9ed5",
7835
+ accent5: "#a02b93",
7836
+ accent6: "#4ea72e"
7837
+ };
7838
+ return colors[value];
7839
+ }
7584
7840
  function renderChartPreviewSection(charts) {
7585
7841
  const section = createSection("\u8868\u683C\u56FE\u8868\u9884\u89C8");
7586
7842
  const grid = document.createElement("div");
@@ -7618,17 +7874,39 @@ function renderChartCard(chart) {
7618
7874
  }
7619
7875
  function renderChartSvg(chart) {
7620
7876
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
7621
- svg.setAttribute("viewBox", "0 0 640 260");
7877
+ svg.setAttribute("viewBox", "0 0 640 380");
7622
7878
  svg.setAttribute("role", "img");
7623
7879
  svg.setAttribute("aria-label", chart.title);
7624
7880
  svg.classList.add("ofv-chart-svg");
7625
7881
  const allValues = chart.series.flatMap((item) => item.values).filter((value) => Number.isFinite(value));
7626
7882
  const max = Math.max(1, ...allValues);
7627
7883
  const min = Math.min(0, ...allValues);
7884
+ const axisScale = createChartAxisScale(max, min);
7885
+ const axisMax = axisScale.max;
7886
+ const axisMin = axisScale.min;
7628
7887
  const span = max - min || 1;
7629
- const colors = ["#2563eb", "#059669", "#d97706", "#dc2626", "#7c3aed"];
7630
- const plot = { x: 48, y: 24, width: 552, height: 178 };
7631
- appendSvg(svg, "line", { x1: plot.x, y1: plot.y, x2: plot.x, y2: plot.y + plot.height, class: "ofv-chart-axis" });
7888
+ const colors = ["#156082", "#e97132", "#196b24", "#0f9ed5", "#a02b93", "#4ea72e"];
7889
+ const plot = { x: 74, y: 74, width: 526, height: 214 };
7890
+ const categories = chart.categories.length > 0 ? chart.categories : chart.series[0]?.values.map((_, index) => String(index + 1)) || [];
7891
+ const title = appendSvg(svg, "text", { x: 320, y: 34, class: "ofv-chart-title", "text-anchor": "middle" });
7892
+ title.textContent = chart.title;
7893
+ for (const value of axisScale.ticks) {
7894
+ const y = plot.y + plot.height - (value - axisMin) / (axisMax - axisMin || 1) * plot.height;
7895
+ appendSvg(svg, "line", {
7896
+ x1: plot.x,
7897
+ y1: Number(y.toFixed(1)),
7898
+ x2: plot.x + plot.width,
7899
+ y2: Number(y.toFixed(1)),
7900
+ class: value === 0 ? "ofv-chart-axis" : "ofv-chart-gridline"
7901
+ });
7902
+ const label = appendSvg(svg, "text", {
7903
+ x: plot.x - 12,
7904
+ y: Number((y + 4).toFixed(1)),
7905
+ class: "ofv-chart-label",
7906
+ "text-anchor": "end"
7907
+ });
7908
+ label.textContent = formatChartTick(value);
7909
+ }
7632
7910
  appendSvg(svg, "line", {
7633
7911
  x1: plot.x,
7634
7912
  y1: plot.y + plot.height,
@@ -7636,28 +7914,47 @@ function renderChartSvg(chart) {
7636
7914
  y2: plot.y + plot.height,
7637
7915
  class: "ofv-chart-axis"
7638
7916
  });
7639
- chart.series.forEach((series, seriesIndex) => {
7640
- const color = colors[seriesIndex % colors.length];
7641
- const step = series.values.length > 1 ? plot.width / (series.values.length - 1) : plot.width;
7642
- const points = series.values.map((value, index) => ({
7643
- x: plot.x + index * step,
7644
- y: plot.y + plot.height - (value - min) / span * plot.height
7645
- }));
7646
- if (chart.type.includes("bar") || chart.type.includes("col")) {
7647
- const barWidth = Math.max(4, Math.min(28, step * 0.6)) / Math.max(1, chart.series.length);
7648
- points.forEach((point, index) => {
7649
- const zeroY = plot.y + plot.height - (0 - min) / span * plot.height;
7650
- const x = point.x - barWidth * chart.series.length / 2 + seriesIndex * barWidth;
7917
+ if (chart.type.includes("bar") || chart.type.includes("col")) {
7918
+ const categoryCount = Math.max(1, categories.length, ...chart.series.map((series) => series.values.length));
7919
+ const groupWidth = plot.width / categoryCount;
7920
+ const clusterWidth = groupWidth * 0.58;
7921
+ const barWidth = Math.max(5, Math.min(28, clusterWidth / Math.max(1, chart.series.length)));
7922
+ const zeroY = plot.y + plot.height - (0 - axisMin) / (axisMax - axisMin || 1) * plot.height;
7923
+ categories.slice(0, categoryCount).forEach((category, index) => {
7924
+ const x = plot.x + groupWidth * (index + 0.5);
7925
+ const label = appendSvg(svg, "text", {
7926
+ x: Number(x.toFixed(1)),
7927
+ y: plot.y + plot.height + 22,
7928
+ class: "ofv-chart-label",
7929
+ "text-anchor": "middle"
7930
+ });
7931
+ label.textContent = truncateChartLabel(category);
7932
+ });
7933
+ chart.series.forEach((series, seriesIndex) => {
7934
+ const color = series.color || colors[seriesIndex % colors.length];
7935
+ series.values.forEach((value, index) => {
7936
+ const groupCenter = plot.x + groupWidth * (index + 0.5);
7937
+ const x = groupCenter - barWidth * chart.series.length / 2 + seriesIndex * barWidth + barWidth * 0.12;
7938
+ const y = plot.y + plot.height - (value - axisMin) / (axisMax - axisMin || 1) * plot.height;
7651
7939
  appendSvg(svg, "rect", {
7652
- x,
7653
- y: Math.min(point.y, zeroY),
7654
- width: barWidth,
7655
- height: Math.max(1, Math.abs(zeroY - point.y)),
7940
+ x: Number(x.toFixed(1)),
7941
+ y: Number(Math.min(y, zeroY).toFixed(1)),
7942
+ width: Number((barWidth * 0.76).toFixed(1)),
7943
+ height: Number(Math.max(1, Math.abs(zeroY - y)).toFixed(1)),
7656
7944
  fill: color,
7657
7945
  "data-index": index
7658
7946
  });
7659
7947
  });
7660
- } else {
7948
+ });
7949
+ } else {
7950
+ chart.series.forEach((series, seriesIndex) => {
7951
+ const color = series.color || colors[seriesIndex % colors.length];
7952
+ const step = series.values.length > 1 ? plot.width / (series.values.length - 1) : plot.width;
7953
+ const lineSpan = axisMax - axisMin || span;
7954
+ const points = series.values.map((value, index) => ({
7955
+ x: plot.x + index * step,
7956
+ y: plot.y + plot.height - (value - axisMin) / lineSpan * plot.height
7957
+ }));
7661
7958
  appendSvg(svg, "polyline", {
7662
7959
  points: points.map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(" "),
7663
7960
  fill: "none",
@@ -7669,11 +7966,49 @@ function renderChartSvg(chart) {
7669
7966
  for (const point of points.slice(0, 80)) {
7670
7967
  appendSvg(svg, "circle", { cx: point.x, cy: point.y, r: 3, fill: color });
7671
7968
  }
7672
- }
7673
- appendLegend(svg, series.name, color, 52 + seriesIndex * 118, 236);
7674
- });
7969
+ });
7970
+ }
7971
+ appendChartLegend(svg, chart, colors, 348);
7675
7972
  return svg;
7676
7973
  }
7974
+ function appendChartLegend(svg, chart, colors, y) {
7975
+ const itemWidth = 86;
7976
+ const startX = 320 - chart.series.length * itemWidth / 2;
7977
+ chart.series.forEach((series, seriesIndex) => {
7978
+ const color = series.color || colors[seriesIndex % colors.length];
7979
+ appendLegend(svg, series.name, color, startX + seriesIndex * itemWidth, y);
7980
+ });
7981
+ }
7982
+ function createChartAxisScale(max, min) {
7983
+ const axisMin = Math.min(0, min);
7984
+ const positiveMax = Math.max(1, max);
7985
+ const step = niceChartStep((positiveMax - axisMin) / 5);
7986
+ let axisMax = Math.ceil(positiveMax / step) * step;
7987
+ if (axisMax <= positiveMax) {
7988
+ axisMax += step;
7989
+ }
7990
+ const ticks = [];
7991
+ for (let value = axisMin; value <= axisMax + step / 2; value += step) {
7992
+ ticks.push(Number(value.toFixed(6)));
7993
+ }
7994
+ return { min: axisMin, max: axisMax, ticks };
7995
+ }
7996
+ function niceChartStep(rawStep) {
7997
+ if (rawStep <= 0) {
7998
+ return 1;
7999
+ }
8000
+ const magnitude = 10 ** Math.floor(Math.log10(rawStep));
8001
+ const normalized = rawStep / magnitude;
8002
+ const nice = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
8003
+ return nice * magnitude;
8004
+ }
8005
+ function formatChartTick(value) {
8006
+ const rounded = Math.abs(value) < 1 ? Number(value.toFixed(1)) : Number(value.toFixed(0));
8007
+ return String(rounded);
8008
+ }
8009
+ function truncateChartLabel(value) {
8010
+ return value.length > 10 ? `${value.slice(0, 10)}...` : value;
8011
+ }
7677
8012
  function appendLegend(svg, label, color, x, y) {
7678
8013
  appendSvg(svg, "rect", { x, y: y - 10, width: 12, height: 12, rx: 2, fill: color });
7679
8014
  const text = appendSvg(svg, "text", { x: x + 18, y, class: "ofv-chart-label" });
@@ -8407,7 +8742,7 @@ function renderOpenDocumentPresentationXml(panel, xml) {
8407
8742
  renderPresentationInsight(panel, inspectOpenDocumentPresentation("FODP \u6F14\u793A\u6587\u7A3F", xml, 0));
8408
8743
  renderOpenDocumentPresentation(panel, "FODP \u6F14\u793A\u6587\u7A3F", xml, []);
8409
8744
  }
8410
- async function renderPackagedOfficePreview(panel, arrayBuffer, extension) {
8745
+ async function renderPackagedOfficePreview(panel, arrayBuffer, extension, fit) {
8411
8746
  let zip;
8412
8747
  try {
8413
8748
  zip = await import_jszip3.default.loadAsync(arrayBuffer);
@@ -8418,7 +8753,7 @@ async function renderPackagedOfficePreview(panel, arrayBuffer, extension) {
8418
8753
  const hasEntry = (path) => entries.some((entry) => entry.name.toLowerCase() === path.toLowerCase());
8419
8754
  const contentXml = zip.file(/(^|\/)content\.xml$/i)[0];
8420
8755
  if (hasEntry("word/document.xml")) {
8421
- await renderDocx(panel, arrayBuffer);
8756
+ await renderDocx(panel, arrayBuffer, fit);
8422
8757
  return true;
8423
8758
  }
8424
8759
  if (hasEntry("xl/workbook.xml")) {
@@ -18870,7 +19205,7 @@ async function createPostScriptPreview(bytes, url, fileName, size, fit, toolbar)
18870
19205
  preview.append(summary);
18871
19206
  return { element: preview };
18872
19207
  }
18873
- async function createPdfCompatibleAiPreview(bytes, url, fileName, size, fit, toolbar, pdfOffset = 0) {
19208
+ async function createPdfCompatibleAiPreview(bytes, url, fileName, size, fit, toolbar, pdfOffset = 0, zoom = 1) {
18874
19209
  const wrapper = document.createElement("div");
18875
19210
  wrapper.className = "ofv-ai-pdf-preview";
18876
19211
  let pdfUrl = url;
@@ -18885,6 +19220,7 @@ async function createPdfCompatibleAiPreview(bytes, url, fileName, size, fit, too
18885
19220
  viewport: wrapper,
18886
19221
  size,
18887
19222
  fit,
19223
+ zoom,
18888
19224
  toolbar,
18889
19225
  fallbackTitle: "AI PDF \u517C\u5BB9\u9884\u89C8\u5931\u8D25",
18890
19226
  revokeUrlOnDestroy: false