@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.js CHANGED
@@ -553,6 +553,48 @@ function revokeObjectUrl(url, isExternal) {
553
553
  }
554
554
  }
555
555
 
556
+ // src/messages.ts
557
+ var defaultMessages = {
558
+ "zh-CN": {
559
+ loading: "\u6B63\u5728\u52A0\u8F7D\u9884\u89C8...",
560
+ unsupportedTitle: "\u5F53\u524D\u6587\u4EF6\u6682\u4E0D\u652F\u6301\u5728\u7EBF\u9884\u89C8",
561
+ downloadTitle: "\u5F53\u524D\u6587\u4EF6\u53EF\u4E0B\u8F7D\u540E\u67E5\u770B",
562
+ downloadFile: "\u4E0B\u8F7D\u6587\u4EF6",
563
+ file: "\u6587\u4EF6",
564
+ unnamedFile: "\u672A\u547D\u540D\u6587\u4EF6",
565
+ format: "\u683C\u5F0F",
566
+ unknown: "\u672A\u77E5",
567
+ mime: "MIME",
568
+ undeclared: "\u672A\u58F0\u660E",
569
+ size: "\u5927\u5C0F",
570
+ source: "\u6765\u6E90",
571
+ remoteUrl: "\u8FDC\u7A0B URL",
572
+ localFile: "\u672C\u5730/\u5185\u5B58\u6587\u4EF6"
573
+ },
574
+ "en-US": {
575
+ loading: "Loading preview...",
576
+ unsupportedTitle: "Preview is not available for this file",
577
+ downloadTitle: "This file can be downloaded and opened locally",
578
+ downloadFile: "Download file",
579
+ file: "File",
580
+ unnamedFile: "Untitled file",
581
+ format: "Format",
582
+ unknown: "Unknown",
583
+ mime: "MIME",
584
+ undeclared: "Not declared",
585
+ size: "Size",
586
+ source: "Source",
587
+ remoteUrl: "Remote URL",
588
+ localFile: "Local or in-memory file"
589
+ }
590
+ };
591
+ function resolveMessages(options) {
592
+ return {
593
+ ...defaultMessages[options.locale || "zh-CN"],
594
+ ...options.messages
595
+ };
596
+ }
597
+
556
598
  // src/plugins/fallback.ts
557
599
  function fallbackPlugin() {
558
600
  return {
@@ -570,12 +612,12 @@ function fallbackPlugin() {
570
612
  const panel = document.createElement("div");
571
613
  panel.className = "ofv-fallback";
572
614
  const title = document.createElement("strong");
573
- 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";
574
- const meta = createFallbackMeta(ctx.file);
615
+ title.textContent = ctx.options.fallback === "download" ? ctx.options.messages.downloadTitle : ctx.options.messages.unsupportedTitle;
616
+ const meta = createFallbackMeta(ctx.file, ctx.options.messages);
575
617
  const download = document.createElement("a");
576
618
  download.href = url;
577
619
  download.download = ctx.file.name;
578
- download.textContent = "\u4E0B\u8F7D\u6587\u4EF6";
620
+ download.textContent = ctx.options.messages.downloadFile;
579
621
  panel.append(title, meta, download);
580
622
  ctx.viewport.classList.add("ofv-center");
581
623
  ctx.viewport.append(panel);
@@ -591,14 +633,14 @@ function fallbackPlugin() {
591
633
  }
592
634
  };
593
635
  }
594
- function createFallbackMeta(file) {
636
+ function createFallbackMeta(file, messages) {
595
637
  const meta = document.createElement("dl");
596
638
  meta.className = "ofv-fallback-meta";
597
- appendFallbackMeta(meta, "\u6587\u4EF6", file.name || "\u672A\u547D\u540D\u6587\u4EF6");
598
- appendFallbackMeta(meta, "\u683C\u5F0F", file.extension ? `.${file.extension}` : "\u672A\u77E5");
599
- appendFallbackMeta(meta, "MIME", file.mimeType || "\u672A\u58F0\u660E");
600
- appendFallbackMeta(meta, "\u5927\u5C0F", file.size === void 0 ? "\u672A\u77E5" : formatFallbackBytes(file.size));
601
- appendFallbackMeta(meta, "\u6765\u6E90", file.url ? "\u8FDC\u7A0B URL" : "\u672C\u5730/\u5185\u5B58\u6587\u4EF6");
639
+ appendFallbackMeta(meta, messages.file, file.name || messages.unnamedFile);
640
+ appendFallbackMeta(meta, messages.format, file.extension ? `.${file.extension}` : messages.unknown);
641
+ appendFallbackMeta(meta, messages.mime, file.mimeType || messages.undeclared);
642
+ appendFallbackMeta(meta, messages.size, file.size === void 0 ? messages.unknown : formatFallbackBytes(file.size));
643
+ appendFallbackMeta(meta, messages.source, file.url ? messages.remoteUrl : messages.localFile);
602
644
  return meta;
603
645
  }
604
646
  function appendFallbackMeta(parent, label, value) {
@@ -656,15 +698,17 @@ function createViewer(options) {
656
698
  host.append(status, viewport);
657
699
  container.replaceChildren(host);
658
700
  const normalizedOptions = {
701
+ ...options,
659
702
  fit: options.fit || "contain",
660
703
  fallback: options.fallback || "inline",
661
- ...options
704
+ zoom: normalizeInitialZoom(options.zoom),
705
+ messages: resolveMessages(options)
662
706
  };
663
707
  let destroyed = false;
664
708
  let renderToken = 0;
665
709
  const setLoading = (loading) => {
666
710
  status.hidden = !loading;
667
- status.textContent = loading ? "Loading preview..." : "";
711
+ status.textContent = loading ? normalizedOptions.messages.loading : "";
668
712
  };
669
713
  const setError = (error) => {
670
714
  status.hidden = false;
@@ -820,6 +864,9 @@ function clampIndex(index, length) {
820
864
  }
821
865
  return Math.min(Math.max(index, 0), length - 1);
822
866
  }
867
+ function normalizeInitialZoom(zoom) {
868
+ return typeof zoom === "number" && Number.isFinite(zoom) && zoom > 0 ? zoom : 1;
869
+ }
823
870
  function applyTheme(container, theme) {
824
871
  const media = window.matchMedia?.("(prefers-color-scheme: dark)");
825
872
  const classes = ["ofv-theme-light", "ofv-theme-dark"];
@@ -1696,6 +1743,98 @@ async function findPlugin(plugins, file) {
1696
1743
  return fallbackPlugin();
1697
1744
  }
1698
1745
 
1746
+ // src/plugins/utils.ts
1747
+ async function readArrayBuffer(file) {
1748
+ if (file.source instanceof ArrayBuffer) {
1749
+ return file.source;
1750
+ }
1751
+ if (file.blob) {
1752
+ return file.blob.arrayBuffer();
1753
+ }
1754
+ if (typeof file.source === "string") {
1755
+ const response = await fetch(file.source);
1756
+ if (!response.ok) {
1757
+ throw new Error(`Failed to fetch file: ${response.status}`);
1758
+ }
1759
+ return response.arrayBuffer();
1760
+ }
1761
+ throw new Error("Unsupported file source.");
1762
+ }
1763
+ async function readTextFile(file) {
1764
+ const decode = (buffer) => decodeTextBuffer(buffer);
1765
+ if (typeof file.source === "string") {
1766
+ const response = await fetch(file.source);
1767
+ if (!response.ok) {
1768
+ throw new Error(`Failed to fetch text file: ${response.status}`);
1769
+ }
1770
+ return decode(await response.arrayBuffer());
1771
+ }
1772
+ if (file.blob) {
1773
+ return decode(await file.blob.arrayBuffer());
1774
+ }
1775
+ if (file.source instanceof ArrayBuffer) {
1776
+ return decode(file.source);
1777
+ }
1778
+ return String(file.source);
1779
+ }
1780
+ function createPanel(className = "") {
1781
+ const panel = document.createElement("div");
1782
+ panel.className = `ofv-panel ${className}`.trim();
1783
+ return panel;
1784
+ }
1785
+ function getInitialZoom(ctx, min = 0.1, max = 8) {
1786
+ return Math.min(max, Math.max(min, ctx.options.zoom));
1787
+ }
1788
+ function createSection(title) {
1789
+ const section = document.createElement("section");
1790
+ section.className = "ofv-section";
1791
+ const heading = document.createElement("h3");
1792
+ heading.textContent = title;
1793
+ section.append(heading);
1794
+ return section;
1795
+ }
1796
+ function appendMeta(parent, label, value) {
1797
+ const row = document.createElement("div");
1798
+ row.className = "ofv-meta-row";
1799
+ const key = document.createElement("span");
1800
+ key.textContent = label;
1801
+ const content = document.createElement("strong");
1802
+ content.textContent = String(value);
1803
+ row.append(key, content);
1804
+ parent.append(row);
1805
+ }
1806
+ function decodeTextBuffer(buffer) {
1807
+ return decodeTextBytes(new Uint8Array(buffer));
1808
+ }
1809
+ function decodeTextBytes(bytes) {
1810
+ if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) {
1811
+ return new TextDecoder("utf-8").decode(bytes.subarray(3));
1812
+ }
1813
+ if (bytes.length >= 2) {
1814
+ if (bytes[0] === 255 && bytes[1] === 254) {
1815
+ return new TextDecoder("utf-16le").decode(bytes.subarray(2));
1816
+ }
1817
+ if (bytes[0] === 254 && bytes[1] === 255) {
1818
+ return new TextDecoder("utf-16be").decode(bytes.subarray(2));
1819
+ }
1820
+ }
1821
+ try {
1822
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
1823
+ } catch {
1824
+ return decodeWithFallback(bytes, "gb18030") || decodeWithFallback(bytes, "gbk") || new TextDecoder("utf-8").decode(bytes);
1825
+ }
1826
+ }
1827
+ function decodeWithFallback(bytes, encoding) {
1828
+ try {
1829
+ return new TextDecoder(encoding).decode(bytes);
1830
+ } catch {
1831
+ return void 0;
1832
+ }
1833
+ }
1834
+ function resolveFormat(file, mimeMap) {
1835
+ return file.extension || mimeMap[file.mimeType] || "";
1836
+ }
1837
+
1699
1838
  // src/plugins/image.ts
1700
1839
  var imageExtensions = /* @__PURE__ */ new Set([
1701
1840
  "jpg",
@@ -1738,12 +1877,12 @@ function imagePlugin() {
1738
1877
  let url = "";
1739
1878
  let convertedBlob = null;
1740
1879
  let isExternal = Boolean(ctx.file.url);
1741
- let canvasSource = null;
1880
+ let tiffSource = null;
1742
1881
  if (isTiff) {
1743
1882
  ctx.setLoading(true);
1744
1883
  try {
1745
1884
  const bytes = await sourceBytesPromise;
1746
- canvasSource = await createTiffCanvas(bytes);
1885
+ tiffSource = await createTiffPreview(bytes, ctx.file.name);
1747
1886
  } catch (err) {
1748
1887
  console.error("TIFF image conversion failed:", err);
1749
1888
  url = createObjectUrl(ctx.file);
@@ -1803,13 +1942,16 @@ function imagePlugin() {
1803
1942
  if (url) {
1804
1943
  image.src = url;
1805
1944
  }
1806
- const visual = canvasSource || image;
1807
- if (canvasSource) {
1808
- canvasSource.classList.add("ofv-media", "ofv-image-content", "ofv-tiff-canvas");
1809
- canvasSource.setAttribute("role", "img");
1810
- canvasSource.setAttribute("aria-label", ctx.file.name);
1945
+ const visual = tiffSource || image;
1946
+ if (tiffSource) {
1947
+ tiffSource.classList.add("ofv-image-content");
1948
+ if (tiffSource.classList.contains("ofv-tiff-pages")) {
1949
+ stage.classList.add("ofv-image-stage-pages");
1950
+ }
1811
1951
  }
1812
- let scale = 1;
1952
+ const visualBox = document.createElement("div");
1953
+ visualBox.className = "ofv-image-scrollbox";
1954
+ let scale = getInitialZoom(ctx);
1813
1955
  let rotation = 0;
1814
1956
  let offsetX = 0;
1815
1957
  let offsetY = 0;
@@ -1822,7 +1964,23 @@ function imagePlugin() {
1822
1964
  let previewAvailable = true;
1823
1965
  const zoomLabel = document.createElement("span");
1824
1966
  zoomLabel.className = "ofv-image-zoom";
1967
+ const updateScrollBox = () => {
1968
+ if (visual.classList.contains("ofv-tiff-pages")) {
1969
+ visualBox.style.removeProperty("width");
1970
+ visualBox.style.removeProperty("height");
1971
+ return;
1972
+ }
1973
+ const baseWidth = visual.offsetWidth || visual.getBoundingClientRect().width || visual.naturalWidth || 0;
1974
+ const baseHeight = visual.offsetHeight || visual.getBoundingClientRect().height || visual.naturalHeight || 0;
1975
+ if (baseWidth > 0) {
1976
+ visualBox.style.width = `${Math.ceil(baseWidth * scale)}px`;
1977
+ }
1978
+ if (baseHeight > 0) {
1979
+ visualBox.style.height = `${Math.ceil(baseHeight * scale)}px`;
1980
+ }
1981
+ };
1825
1982
  const updateTransform = () => {
1983
+ updateScrollBox();
1826
1984
  visual.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale}) rotate(${rotation}deg)`;
1827
1985
  zoomLabel.textContent = `${Math.round(scale * 100)}%`;
1828
1986
  ctx.toolbar?.setZoom(previewAvailable ? scale : void 0);
@@ -1942,11 +2100,13 @@ function imagePlugin() {
1942
2100
  stage.addEventListener("lostpointercapture", onLostPointerCapture);
1943
2101
  stage.addEventListener("pointerleave", onPointerLeave);
1944
2102
  stage.addEventListener("wheel", onWheel, { passive: false });
1945
- if (!canvasSource) {
2103
+ if (!tiffSource) {
1946
2104
  image.addEventListener("error", showImageFallback);
2105
+ image.addEventListener("load", updateScrollBox);
1947
2106
  }
1948
2107
  window.addEventListener("blur", onWindowBlur);
1949
- stage.append(visual);
2108
+ visualBox.append(visual);
2109
+ stage.append(visualBox);
1950
2110
  wrapper.append(...showInlineControls ? [controls, stage, infoBar] : [stage, infoBar]);
1951
2111
  ctx.viewport.append(wrapper);
1952
2112
  updateTransform();
@@ -1984,7 +2144,12 @@ function imagePlugin() {
1984
2144
  },
1985
2145
  resize(size) {
1986
2146
  visual.style.maxWidth = `${size.width}px`;
1987
- visual.style.maxHeight = `${Math.max(0, size.height - controls.offsetHeight)}px`;
2147
+ if (visual.classList.contains("ofv-tiff-pages")) {
2148
+ visual.style.removeProperty("max-height");
2149
+ } else {
2150
+ visual.style.maxHeight = `${Math.max(0, size.height - controls.offsetHeight)}px`;
2151
+ }
2152
+ updateScrollBox();
1988
2153
  },
1989
2154
  destroy() {
1990
2155
  ctx.toolbar?.setZoom(void 0);
@@ -1999,6 +2164,7 @@ function imagePlugin() {
1999
2164
  stage.removeEventListener("pointerleave", onPointerLeave);
2000
2165
  stage.removeEventListener("wheel", onWheel);
2001
2166
  image.removeEventListener("error", showImageFallback);
2167
+ image.removeEventListener("load", updateScrollBox);
2002
2168
  window.removeEventListener("blur", onWindowBlur);
2003
2169
  finishDrag();
2004
2170
  wrapper.remove();
@@ -2012,33 +2178,64 @@ function imagePlugin() {
2012
2178
  }
2013
2179
  };
2014
2180
  }
2015
- async function createTiffCanvas(bytes) {
2181
+ async function createTiffPreview(bytes, fileName) {
2016
2182
  if (bytes.byteLength === 0) {
2017
2183
  throw new Error("\u65E0\u6CD5\u8BFB\u53D6 TIFF \u6587\u4EF6\u5185\u5BB9\u3002");
2018
2184
  }
2019
2185
  const UTIF = await import("utif");
2020
2186
  const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
2021
2187
  const ifds = UTIF.decode(buffer);
2022
- const ifd = ifds.find((item) => Number(item.width || 0) > 0 || Number(item.t256 || 0) > 0) || ifds[0];
2023
- if (!ifd) {
2188
+ const imageIfds = ifds.filter((item) => hasTiffDimensions(item));
2189
+ if (imageIfds.length === 0) {
2024
2190
  throw new Error("TIFF \u6587\u4EF6\u6CA1\u6709\u53EF\u89E3\u7801\u7684\u56FE\u50CF\u76EE\u5F55\u3002");
2025
2191
  }
2192
+ const pages = imageIfds.map((ifd, index) => createTiffPage(UTIF, buffer, ifd, index, imageIfds.length, fileName));
2193
+ if (pages.length === 1) {
2194
+ return pages[0].canvas;
2195
+ }
2196
+ const container = document.createElement("div");
2197
+ container.className = "ofv-tiff-pages";
2198
+ container.setAttribute("role", "group");
2199
+ container.setAttribute("aria-label", `${fileName}\uFF0C\u5171 ${pages.length} \u9875`);
2200
+ for (const page of pages) {
2201
+ const figure = document.createElement("figure");
2202
+ figure.className = "ofv-tiff-page";
2203
+ const caption = document.createElement("figcaption");
2204
+ caption.textContent = `\u7B2C ${page.index + 1} / ${pages.length} \u9875 \xB7 ${page.width} x ${page.height}px`;
2205
+ figure.append(page.canvas, caption);
2206
+ container.append(figure);
2207
+ }
2208
+ return container;
2209
+ }
2210
+ function createTiffPage(UTIF, buffer, ifd, index, total, fileName) {
2026
2211
  UTIF.decodeImage(buffer, ifd);
2027
2212
  const rgba = UTIF.toRGBA8(ifd);
2028
- const width = Number(ifd.width || ifd.t256 || 0);
2029
- const height = Number(ifd.height || ifd.t257 || 0);
2213
+ const { width, height } = getTiffDimensions(ifd);
2030
2214
  if (!width || !height || rgba.length < width * height * 4) {
2031
2215
  throw new Error("TIFF \u56FE\u50CF\u50CF\u7D20\u6570\u636E\u4E0D\u5B8C\u6574\u3002");
2032
2216
  }
2033
2217
  const canvas = document.createElement("canvas");
2218
+ canvas.className = "ofv-tiff-canvas";
2034
2219
  canvas.width = width;
2035
2220
  canvas.height = height;
2221
+ canvas.setAttribute("role", "img");
2222
+ canvas.setAttribute("aria-label", total > 1 ? `${fileName} \u7B2C ${index + 1} \u9875` : fileName);
2036
2223
  const context = canvas.getContext("2d");
2037
2224
  if (!context) {
2038
2225
  throw new Error("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301 Canvas 2D\uFF0C\u65E0\u6CD5\u5C55\u793A TIFF\u3002");
2039
2226
  }
2040
2227
  context.putImageData(new ImageData(new Uint8ClampedArray(rgba), width, height), 0, 0);
2041
- return canvas;
2228
+ return { canvas, width, height, index };
2229
+ }
2230
+ function hasTiffDimensions(ifd) {
2231
+ const { width, height } = getTiffDimensions(ifd);
2232
+ return width > 0 && height > 0;
2233
+ }
2234
+ function getTiffDimensions(ifd) {
2235
+ return {
2236
+ width: Number(ifd.width || ifd.t256 || 0),
2237
+ height: Number(ifd.height || ifd.t257 || 0)
2238
+ };
2042
2239
  }
2043
2240
  function createImageFallback(fileName, url) {
2044
2241
  const fallback = document.createElement("div");
@@ -2367,6 +2564,7 @@ function parseTiffInfo(bytes) {
2367
2564
  return { format: magic === 43 ? "BigTIFF" : "TIFF", note: "IFD \u504F\u79FB\u8D85\u51FA\u6587\u4EF6\u8303\u56F4" };
2368
2565
  }
2369
2566
  const count = magic === 43 ? readTiffUint64AsNumber(bytes, ifdOffset, littleEndian) : readTiffUint16(bytes, ifdOffset, littleEndian);
2567
+ const imageCount = countTiffIfds(bytes, ifdOffset, magic === 43, littleEndian);
2370
2568
  const entrySize = magic === 43 ? 20 : 12;
2371
2569
  const entriesStart = ifdOffset + (magic === 43 ? 8 : 2);
2372
2570
  let width;
@@ -2393,9 +2591,26 @@ function parseTiffInfo(bytes) {
2393
2591
  height,
2394
2592
  bitDepth: bitDepth ? `${bitDepth} bit` : void 0,
2395
2593
  color: compression ? tiffCompressionName(compression) : void 0,
2396
- count: Number.isFinite(count) ? count : void 0
2594
+ count: imageCount || void 0
2397
2595
  };
2398
2596
  }
2597
+ function countTiffIfds(bytes, firstOffset, bigTiff, littleEndian) {
2598
+ const seen = /* @__PURE__ */ new Set();
2599
+ let offset = firstOffset;
2600
+ let count = 0;
2601
+ while (Number.isFinite(offset) && offset > 0 && offset < bytes.length && !seen.has(offset) && count < 512) {
2602
+ seen.add(offset);
2603
+ const entryCount = bigTiff ? readTiffUint64AsNumber(bytes, offset, littleEndian) : readTiffUint16(bytes, offset, littleEndian);
2604
+ const entrySize = bigTiff ? 20 : 12;
2605
+ const nextOffsetPosition = offset + (bigTiff ? 8 : 2) + entryCount * entrySize;
2606
+ if (!Number.isFinite(entryCount) || nextOffsetPosition + (bigTiff ? 8 : 4) > bytes.length) {
2607
+ break;
2608
+ }
2609
+ count++;
2610
+ offset = bigTiff ? readTiffUint64AsNumber(bytes, nextOffsetPosition, littleEndian) : readTiffUint32(bytes, nextOffsetPosition, littleEndian);
2611
+ }
2612
+ return count;
2613
+ }
2399
2614
  function readTiffInlineValue(bytes, offset, type, littleEndian) {
2400
2615
  if (offset + 4 > bytes.length) {
2401
2616
  return void 0;
@@ -3637,95 +3852,6 @@ function readAiffExtended(bytes, offset) {
3637
3852
  return mantissa * Math.pow(2, exponent - 16383 - 63);
3638
3853
  }
3639
3854
 
3640
- // src/plugins/utils.ts
3641
- async function readArrayBuffer(file) {
3642
- if (file.source instanceof ArrayBuffer) {
3643
- return file.source;
3644
- }
3645
- if (file.blob) {
3646
- return file.blob.arrayBuffer();
3647
- }
3648
- if (typeof file.source === "string") {
3649
- const response = await fetch(file.source);
3650
- if (!response.ok) {
3651
- throw new Error(`Failed to fetch file: ${response.status}`);
3652
- }
3653
- return response.arrayBuffer();
3654
- }
3655
- throw new Error("Unsupported file source.");
3656
- }
3657
- async function readTextFile(file) {
3658
- const decode = (buffer) => decodeTextBuffer(buffer);
3659
- if (typeof file.source === "string") {
3660
- const response = await fetch(file.source);
3661
- if (!response.ok) {
3662
- throw new Error(`Failed to fetch text file: ${response.status}`);
3663
- }
3664
- return decode(await response.arrayBuffer());
3665
- }
3666
- if (file.blob) {
3667
- return decode(await file.blob.arrayBuffer());
3668
- }
3669
- if (file.source instanceof ArrayBuffer) {
3670
- return decode(file.source);
3671
- }
3672
- return String(file.source);
3673
- }
3674
- function createPanel(className = "") {
3675
- const panel = document.createElement("div");
3676
- panel.className = `ofv-panel ${className}`.trim();
3677
- return panel;
3678
- }
3679
- function createSection(title) {
3680
- const section = document.createElement("section");
3681
- section.className = "ofv-section";
3682
- const heading = document.createElement("h3");
3683
- heading.textContent = title;
3684
- section.append(heading);
3685
- return section;
3686
- }
3687
- function appendMeta(parent, label, value) {
3688
- const row = document.createElement("div");
3689
- row.className = "ofv-meta-row";
3690
- const key = document.createElement("span");
3691
- key.textContent = label;
3692
- const content = document.createElement("strong");
3693
- content.textContent = String(value);
3694
- row.append(key, content);
3695
- parent.append(row);
3696
- }
3697
- function decodeTextBuffer(buffer) {
3698
- return decodeTextBytes(new Uint8Array(buffer));
3699
- }
3700
- function decodeTextBytes(bytes) {
3701
- if (bytes.length >= 3 && bytes[0] === 239 && bytes[1] === 187 && bytes[2] === 191) {
3702
- return new TextDecoder("utf-8").decode(bytes.subarray(3));
3703
- }
3704
- if (bytes.length >= 2) {
3705
- if (bytes[0] === 255 && bytes[1] === 254) {
3706
- return new TextDecoder("utf-16le").decode(bytes.subarray(2));
3707
- }
3708
- if (bytes[0] === 254 && bytes[1] === 255) {
3709
- return new TextDecoder("utf-16be").decode(bytes.subarray(2));
3710
- }
3711
- }
3712
- try {
3713
- return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
3714
- } catch {
3715
- return decodeWithFallback(bytes, "gb18030") || decodeWithFallback(bytes, "gbk") || new TextDecoder("utf-8").decode(bytes);
3716
- }
3717
- }
3718
- function decodeWithFallback(bytes, encoding) {
3719
- try {
3720
- return new TextDecoder(encoding).decode(bytes);
3721
- } catch {
3722
- return void 0;
3723
- }
3724
- }
3725
- function resolveFormat(file, mimeMap) {
3726
- return file.extension || mimeMap[file.mimeType] || "";
3727
- }
3728
-
3729
3855
  // src/plugins/text.ts
3730
3856
  var langMap = {
3731
3857
  js: "javascript",
@@ -3883,26 +4009,6 @@ var mimeLangMap = {
3883
4009
  };
3884
4010
  var MAX_HIGHLIGHT_CHARS = 18e4;
3885
4011
  var MAX_RENDER_CHARS = 6e5;
3886
- function loadPrismCss(theme) {
3887
- const lightId = "ofv-prism-css-light";
3888
- const darkId = "ofv-prism-css-dark";
3889
- const activeId = theme === "dark" ? darkId : lightId;
3890
- const inactiveId = theme === "dark" ? lightId : darkId;
3891
- document.getElementById(inactiveId)?.remove();
3892
- if (document.getElementById(activeId)) {
3893
- return Promise.resolve();
3894
- }
3895
- 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";
3896
- return new Promise((resolve, reject) => {
3897
- const link = document.createElement("link");
3898
- link.id = activeId;
3899
- link.rel = "stylesheet";
3900
- link.href = href;
3901
- link.onload = () => resolve();
3902
- link.onerror = () => reject(new Error(`Failed to load Prism CSS: ${href}`));
3903
- document.head.appendChild(link);
3904
- });
3905
- }
3906
4012
  function textPlugin() {
3907
4013
  return {
3908
4014
  name: "text",
@@ -3926,7 +4032,6 @@ function textPlugin() {
3926
4032
  }
3927
4033
  };
3928
4034
  }
3929
- 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";
3930
4035
  if (isMarkdown) {
3931
4036
  const [markedModule, PrismModule2, DOMPurifyModule] = await Promise.all([
3932
4037
  import("marked"),
@@ -3947,7 +4052,6 @@ function textPlugin() {
3947
4052
  try {
3948
4053
  const codeBlocks = container.querySelectorAll("pre code");
3949
4054
  if (codeBlocks.length > 0) {
3950
- await loadPrismCss(isDark ? "dark" : "light");
3951
4055
  codeBlocks.forEach((block) => {
3952
4056
  const parent = block.parentElement;
3953
4057
  if (parent && !parent.className.includes("language-")) {
@@ -4071,9 +4175,6 @@ function textPlugin() {
4071
4175
  console.warn(`Prism failed to load language component for: ${lang}`, e);
4072
4176
  }
4073
4177
  }
4074
- await loadPrismCss(isDark ? "dark" : "light").catch((error) => {
4075
- console.warn("Prism CSS failed to load; rendering code without external theme:", error);
4076
- });
4077
4178
  const codeText = text.length > MAX_RENDER_CHARS ? text.slice(0, MAX_RENDER_CHARS) : text;
4078
4179
  const totalLines = countLines(text);
4079
4180
  const shownLines = countLines(codeText);
@@ -4194,7 +4295,7 @@ function textPlugin() {
4194
4295
  };
4195
4296
  }
4196
4297
  function createTextZoomController(target, cssVariable, ctx) {
4197
- let zoom = 1;
4298
+ let zoom = getInitialZoom(ctx, 0.5, 3);
4198
4299
  const apply = () => {
4199
4300
  const normalized = Math.round(zoom * 100) / 100;
4200
4301
  target.style.setProperty(cssVariable, String(normalized));
@@ -4504,6 +4605,7 @@ function pdfPlugin(options = {}) {
4504
4605
  viewport: ctx.viewport,
4505
4606
  size: ctx.size,
4506
4607
  fit: ctx.options.fit,
4608
+ zoom: ctx.options.zoom,
4507
4609
  toolbar: ctx.toolbar,
4508
4610
  encryptedTitle: "PDF \u5DF2\u52A0\u5BC6\uFF0C\u65E0\u6CD5\u5728\u7EBF\u9884\u89C8",
4509
4611
  encryptedMessage: "\u8BF7\u4E0B\u8F7D\u540E\u4F7F\u7528\u5BC6\u7801\u6253\u5F00\uFF0C\u6216\u4E0A\u4F20\u89E3\u5BC6\u540E\u7684 PDF \u6587\u4EF6\u3002",
@@ -4600,16 +4702,17 @@ async function renderPdfDocumentPreview(options) {
4600
4702
  const baseViewport = page.getViewport({ scale: 1 });
4601
4703
  pagesMeta.push({
4602
4704
  width: baseViewport.width,
4603
- height: baseViewport.height
4705
+ height: baseViewport.height,
4706
+ rotation: getPdfPageRotation(page)
4604
4707
  });
4605
4708
  } catch {
4606
- pagesMeta.push({ width: 612, height: 792 });
4709
+ pagesMeta.push({ width: 612, height: 792, rotation: 0 });
4607
4710
  }
4608
4711
  }
4609
4712
  const pageStates = [];
4610
4713
  let observer = null;
4611
4714
  let currentSize = options.size;
4612
- let zoomFactor = 1;
4715
+ let zoomFactor = getInitialZoom({ options: { zoom: options.zoom ?? 1 } }, 0.25, 4);
4613
4716
  let rotation = 0;
4614
4717
  const updateSummary = () => {
4615
4718
  renderPdfSummary(summary, pdfDocument.numPages, pagesMeta, options.fit, zoomFactor);
@@ -4638,7 +4741,7 @@ async function renderPdfDocumentPreview(options) {
4638
4741
  const page = await pdfDocument.getPage(pageIdx + 1);
4639
4742
  const meta = pagesMeta[pageIdx];
4640
4743
  const scale = options.fit === "actual" ? zoomFactor : Math.max(0.05, Math.min(5, getPdfAvailableWidth(size.width) / rotatedPdfWidth(meta, rotation) * zoomFactor));
4641
- const viewport = page.getViewport({ scale, rotation });
4744
+ const viewport = page.getViewport({ scale, rotation: getPdfRenderRotation(meta, rotation) });
4642
4745
  const outputScale = getPdfOutputScale();
4643
4746
  const cssWidth = Math.floor(viewport.width);
4644
4747
  const cssHeight = Math.floor(viewport.height);
@@ -4677,6 +4780,8 @@ async function renderPdfDocumentPreview(options) {
4677
4780
  const span = document.createElement("span");
4678
4781
  span.textContent = str;
4679
4782
  span.style.fontSize = `${fontHeight}px`;
4783
+ span.style.lineHeight = "1";
4784
+ span.style.height = `${fontHeight}px`;
4680
4785
  span.style.fontFamily = item.fontName || "sans-serif";
4681
4786
  span.style.left = `${tx[4]}px`;
4682
4787
  span.style.top = `${tx[5] - fontHeight}px`;
@@ -4874,6 +4979,13 @@ function appendPdfSummary(parent, label, value) {
4874
4979
  function normalizePdfRotation(value) {
4875
4980
  return (value % 360 + 360) % 360;
4876
4981
  }
4982
+ function getPdfPageRotation(page) {
4983
+ const rotation = Number(page.rotate);
4984
+ return Number.isFinite(rotation) ? normalizePdfRotation(rotation) : 0;
4985
+ }
4986
+ function getPdfRenderRotation(meta, userRotation) {
4987
+ return normalizePdfRotation((meta.rotation || 0) + userRotation);
4988
+ }
4877
4989
  function isPdfRotatedSideways(rotation) {
4878
4990
  const normalized = normalizePdfRotation(rotation);
4879
4991
  return normalized === 90 || normalized === 270;
@@ -5844,13 +5956,13 @@ function officePlugin(options = {}) {
5844
5956
  if (conversionContext && await shouldUseOfficeConversion(options, conversionContext)) {
5845
5957
  delegatedInstance = await renderConvertedOfficePreview(panel, ctx, options, conversionContext);
5846
5958
  } else if (packageFormat === "docx" && !fileIsDocx(extension)) {
5847
- disposeDocxFit = await renderDocx(panel, arrayBuffer);
5959
+ disposeDocxFit = await renderDocx(panel, arrayBuffer, ctx.options.fit);
5848
5960
  } else if (packageFormat === "xlsx" && !sheetExtensions.has(extension)) {
5849
5961
  await renderSheet(panel, arrayBuffer, "xlsx");
5850
5962
  } else if (packageFormat === "pptx" && !["pptx", "pptm", "ppsx", "ppsm", "potx", "potm"].includes(extension)) {
5851
5963
  await renderPptx(panel, arrayBuffer);
5852
5964
  } else if (fileIsDocx(extension)) {
5853
- disposeDocxFit = await renderDocx(panel, arrayBuffer);
5965
+ disposeDocxFit = await renderDocx(panel, arrayBuffer, ctx.options.fit);
5854
5966
  } else if (extension === "rtf") {
5855
5967
  renderPlainDocument(panel, "RTF \u6587\u6863", rtfToText(await readTextFromBuffer(arrayBuffer)));
5856
5968
  } else if (extension === "odt") {
@@ -5859,7 +5971,7 @@ function officePlugin(options = {}) {
5859
5971
  renderOpenDocumentXml(panel, "FODT \u6587\u6863", await readTextFromBuffer(arrayBuffer));
5860
5972
  } else if (extension === "fods") {
5861
5973
  renderFlatOds(panel, await readTextFromBuffer(arrayBuffer));
5862
- } else if (packagedOfficeCandidates.has(extension) && await renderPackagedOfficePreview(panel, arrayBuffer, extension)) {
5974
+ } else if (packagedOfficeCandidates.has(extension) && await renderPackagedOfficePreview(panel, arrayBuffer, extension, ctx.options.fit)) {
5863
5975
  } else if (sheetExtensions.has(extension)) {
5864
5976
  await renderSheet(panel, arrayBuffer, extension);
5865
5977
  } else if (["pptx", "pptm", "ppsx", "ppsm", "potx", "potm"].includes(extension)) {
@@ -5934,6 +6046,7 @@ async function renderConvertedOfficePreview(panel, ctx, options, conversionConte
5934
6046
  viewport: panel,
5935
6047
  size: ctx.size,
5936
6048
  fit: ctx.options.fit,
6049
+ zoom: ctx.options.zoom,
5937
6050
  toolbar: ctx.toolbar,
5938
6051
  title: "Office \u9AD8\u4FDD\u771F\u8F6C\u6362\u9884\u89C8",
5939
6052
  fallbackTitle: "Office \u8F6C\u6362\u540E\u7684 PDF \u65E0\u6CD5\u9884\u89C8",
@@ -5997,7 +6110,7 @@ function createOfficeZoomController(panel, ctx) {
5997
6110
  if (!canZoom) {
5998
6111
  return void 0;
5999
6112
  }
6000
- let zoom = 1;
6113
+ let zoom = getInitialZoom(ctx, 0.5, 3);
6001
6114
  const apply = () => {
6002
6115
  panel.style.setProperty("--ofv-office-zoom", String(zoom));
6003
6116
  panel.dispatchEvent(new CustomEvent("ofv-office-zoom"));
@@ -6060,7 +6173,7 @@ async function detectPackagedOfficeFormat(arrayBuffer) {
6060
6173
  }
6061
6174
  return void 0;
6062
6175
  }
6063
- async function renderDocx(panel, arrayBuffer) {
6176
+ async function renderDocx(panel, arrayBuffer, fit) {
6064
6177
  const content = document.createElement("div");
6065
6178
  content.className = "ofv-docx-document";
6066
6179
  const styleContainer = document.createElement("div");
@@ -6099,7 +6212,7 @@ async function renderDocx(panel, arrayBuffer) {
6099
6212
  return () => void 0;
6100
6213
  }
6101
6214
  panel.append(content);
6102
- disposeFit = fitDocxPages(content);
6215
+ disposeFit = fitDocxPages(content, fit);
6103
6216
  return () => {
6104
6217
  disposeFit?.();
6105
6218
  styleContainer.remove();
@@ -6697,7 +6810,8 @@ function looksLikeDocxTextboxHeading(value) {
6697
6810
  return text.length > 0 && text.length <= 12 && !/[0-9@.::]/.test(text);
6698
6811
  }
6699
6812
  async function normalizeDocxLayout(container, arrayBuffer) {
6700
- const hints = await readDocxLayoutHints(arrayBuffer);
6813
+ const [hints, charts] = await Promise.all([readDocxLayoutHints(arrayBuffer), readDocxCharts(arrayBuffer)]);
6814
+ repairDocxChartPlaceholders(container, charts);
6701
6815
  const pages = container.querySelectorAll("section.ofv-docx");
6702
6816
  for (const page of pages) {
6703
6817
  repairDocxShapeFills(page);
@@ -6712,6 +6826,75 @@ async function normalizeDocxLayout(container, arrayBuffer) {
6712
6826
  }
6713
6827
  }
6714
6828
  }
6829
+ async function readDocxCharts(arrayBuffer) {
6830
+ try {
6831
+ const zip = await JSZip3.loadAsync(arrayBuffer);
6832
+ const documentXml = await zip.file("word/document.xml")?.async("text");
6833
+ const documentDoc = documentXml ? parseOfficeXml(documentXml) : void 0;
6834
+ if (!documentDoc) {
6835
+ return [];
6836
+ }
6837
+ const relationships = await readOfficeRelationships(zip, "word/document.xml");
6838
+ const chartDrawings = Array.from(documentDoc.getElementsByTagName("*")).filter((element) => element.localName === "inline" || element.localName === "anchor").map((element) => readDocxChartDrawing(element)).filter((item) => Boolean(item?.relationshipId));
6839
+ const charts = [];
6840
+ for (const [index, drawing] of chartDrawings.entries()) {
6841
+ const chartRel = relationships.find((rel) => rel.id === drawing.relationshipId && /\/chart$/i.test(rel.type));
6842
+ const chartPath = resolveOfficeRelationshipTarget("word/document.xml", chartRel?.target);
6843
+ const chartXml = chartPath ? await zip.file(chartPath)?.async("text") : void 0;
6844
+ const chart = chartXml ? parseChartXml(chartXml, chartPath?.split("/").pop() || `chart${index + 1}.xml`) : null;
6845
+ if (chart) {
6846
+ charts.push({ ...chart, widthPt: drawing.widthPt, heightPt: drawing.heightPt });
6847
+ }
6848
+ }
6849
+ return charts;
6850
+ } catch {
6851
+ return [];
6852
+ }
6853
+ }
6854
+ function readDocxChartDrawing(element) {
6855
+ const chart = Array.from(element.getElementsByTagName("*")).find((child) => child.localName === "chart");
6856
+ const relationshipId = chart ? getXmlAttribute3(chart, "id") : "";
6857
+ if (!relationshipId) {
6858
+ return void 0;
6859
+ }
6860
+ const extent = Array.from(element.children).find((child) => child.localName === "extent");
6861
+ return {
6862
+ relationshipId,
6863
+ widthPt: emuToPt(Number(extent?.getAttribute("cx") || 0)),
6864
+ heightPt: emuToPt(Number(extent?.getAttribute("cy") || 0))
6865
+ };
6866
+ }
6867
+ function repairDocxChartPlaceholders(container, charts) {
6868
+ if (charts.length === 0) {
6869
+ return;
6870
+ }
6871
+ const placeholders = Array.from(container.querySelectorAll("section.ofv-docx div")).filter(isDocxChartPlaceholder);
6872
+ if (placeholders.length !== charts.length) {
6873
+ return;
6874
+ }
6875
+ placeholders.forEach((placeholder, index) => {
6876
+ const chart = charts[index];
6877
+ placeholder.classList.add("ofv-docx-chart-preview");
6878
+ placeholder.dataset.ofvDocxChartPreview = "true";
6879
+ if (chart.widthPt > 0) {
6880
+ placeholder.style.width ||= `${formatCssNumber(chart.widthPt)}pt`;
6881
+ }
6882
+ if (chart.heightPt > 0) {
6883
+ placeholder.style.height ||= `${formatCssNumber(chart.heightPt)}pt`;
6884
+ }
6885
+ placeholder.replaceChildren(renderChartSvg(chart));
6886
+ });
6887
+ }
6888
+ function isDocxChartPlaceholder(element) {
6889
+ if (element.dataset.ofvDocxChartPreview === "true" || element.children.length > 0 || normalizePreviewText(element.textContent || "")) {
6890
+ return false;
6891
+ }
6892
+ const display = element.style.display;
6893
+ const position = element.style.position;
6894
+ const width = parseCssPixelValue(element.style.width);
6895
+ const height = parseCssPixelValue(element.style.height);
6896
+ return display === "inline-block" && position === "relative" && width >= 120 && height >= 80;
6897
+ }
6715
6898
  function repairDocxHeadingShapeAlignment(page) {
6716
6899
  for (const paragraph of page.querySelectorAll("p")) {
6717
6900
  const text = normalizePreviewText(paragraph.textContent || "");
@@ -6862,14 +7045,19 @@ function repairDocxShapeFills(page) {
6862
7045
  }
6863
7046
  }
6864
7047
  function repairDocxFloatingPictures(page, hints) {
6865
- const hint = hints.floatingPictures.find((item) => item.relativeFrom === "column" && item.wrap === "square");
6866
- if (!hint) {
7048
+ const pageHints = hints.floatingPictures.filter((item) => item.relativeFrom === "column" && item.wrap === "square");
7049
+ if (pageHints.length === 0) {
6867
7050
  return;
6868
7051
  }
6869
- const image = page.querySelector("img");
6870
- if (!image) {
7052
+ const images = Array.from(page.querySelectorAll("img")).filter(
7053
+ (image) => image.closest("[data-ofv-docx-float-repaired='true']") === null
7054
+ );
7055
+ if (images.length === 0 || images.length !== pageHints.length) {
6871
7056
  return;
6872
7057
  }
7058
+ images.forEach((image, index) => repairDocxFloatingPicture(page, image, pageHints[index]));
7059
+ }
7060
+ function repairDocxFloatingPicture(page, image, hint) {
6873
7061
  const wrapper = image.parentElement;
6874
7062
  if (!wrapper || wrapper.dataset.ofvDocxFloatRepaired === "true") {
6875
7063
  return;
@@ -6915,7 +7103,7 @@ function parseCssLineHeight(value) {
6915
7103
  const parsed = Number.parseFloat(trimmed);
6916
7104
  return Number.isFinite(parsed) ? parsed : 0;
6917
7105
  }
6918
- function fitDocxPages(container) {
7106
+ function fitDocxPages(container, fit) {
6919
7107
  const wrapper = container.querySelector(".ofv-docx-wrapper");
6920
7108
  if (!wrapper) {
6921
7109
  return () => void 0;
@@ -6928,6 +7116,7 @@ function fitDocxPages(container) {
6928
7116
  return;
6929
7117
  }
6930
7118
  const availableWidth = Math.max(1, container.clientWidth - 48);
7119
+ const availableHeight = container.clientHeight > 0 ? Math.max(1, container.clientHeight - 48) : void 0;
6931
7120
  const pageWidth = Math.max(
6932
7121
  1,
6933
7122
  ...frames.map(({ page }) => {
@@ -6935,14 +7124,24 @@ function fitDocxPages(container) {
6935
7124
  return page.offsetWidth || rectWidth || parseCssPixelValue(page.style.width) || 794;
6936
7125
  })
6937
7126
  );
6938
- const scale = Math.min(1, Math.max(0.35, availableWidth / pageWidth));
7127
+ const pageHeight = Math.max(
7128
+ 1,
7129
+ ...frames.map(({ page }) => page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height) || 1123)
7130
+ );
7131
+ const scale = getDocxFitScale(fit, availableWidth, availableHeight, pageWidth, pageHeight);
6939
7132
  const userZoom = parseCssPixelValue(panel?.style.getPropertyValue("--ofv-office-zoom") || "1") || 1;
6940
7133
  wrapper.style.setProperty("--ofv-docx-scale", formatCssNumber(scale));
6941
7134
  wrapper.style.setProperty("--ofv-docx-page-width", `${pageWidth}px`);
7135
+ wrapper.style.width = `${Math.ceil(pageWidth * scale * userZoom + 48)}px`;
7136
+ wrapper.style.maxWidth = "none";
7137
+ wrapper.style.overflow = "visible";
6942
7138
  for (const { frame, page } of frames) {
6943
- const pageHeight = page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height);
6944
- if (pageHeight > 0) {
6945
- frame.style.height = `${Math.ceil(pageHeight * scale * userZoom)}px`;
7139
+ const framePageHeight = page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height);
7140
+ const framePageWidth = page.offsetWidth || page.getBoundingClientRect().width || parseCssPixelValue(page.style.width) || pageWidth;
7141
+ frame.style.width = `${Math.ceil(framePageWidth * scale * userZoom)}px`;
7142
+ frame.style.maxWidth = "none";
7143
+ if (framePageHeight > 0) {
7144
+ frame.style.height = `${Math.ceil(framePageHeight * scale * userZoom)}px`;
6946
7145
  }
6947
7146
  }
6948
7147
  };
@@ -6967,6 +7166,26 @@ function fitDocxPages(container) {
6967
7166
  observer.disconnect();
6968
7167
  };
6969
7168
  }
7169
+ function getDocxFitScale(fit, availableWidth, availableHeight, pageWidth, pageHeight) {
7170
+ const widthScale = availableWidth / pageWidth;
7171
+ const heightScale = availableHeight ? availableHeight / pageHeight : void 0;
7172
+ if (fit === "actual") {
7173
+ return 1;
7174
+ }
7175
+ if (fit === "height") {
7176
+ return Math.max(0.1, heightScale ?? widthScale);
7177
+ }
7178
+ if (fit === "cover") {
7179
+ return Math.max(0.1, Math.max(widthScale, heightScale ?? widthScale));
7180
+ }
7181
+ if (fit === "scale-down") {
7182
+ return Math.min(1, Math.max(0.1, Math.min(widthScale, heightScale ?? widthScale)));
7183
+ }
7184
+ if (fit === "contain") {
7185
+ return Math.max(0.1, Math.min(widthScale, heightScale ?? widthScale));
7186
+ }
7187
+ return Math.max(0.1, widthScale);
7188
+ }
6970
7189
  function ensureDocxPageFrames(wrapper) {
6971
7190
  const pages = Array.from(wrapper.querySelectorAll("section.ofv-docx"));
6972
7191
  return pages.map((page) => {
@@ -7498,9 +7717,9 @@ function parseChartXml(xml, fallbackName) {
7498
7717
  return null;
7499
7718
  }
7500
7719
  const type = detectChartType(doc);
7501
- const title = textFromFirst(doc, "title") || fallbackName.replace(/\.xml$/i, "");
7720
+ const title = readChartTitle(doc, fallbackName);
7502
7721
  const seriesElements = Array.from(doc.getElementsByTagName("*")).filter((element) => element.localName === "ser");
7503
- const series = seriesElements.map((element, index) => parseChartSeries(element, index)).filter((item) => item.values.length > 0);
7722
+ const series = seriesElements.map((element, index) => ({ ...parseChartSeries(element, index), color: readChartSeriesColor(element) })).filter((item) => item.values.length > 0);
7504
7723
  if (series.length === 0) {
7505
7724
  return null;
7506
7725
  }
@@ -7509,9 +7728,21 @@ function parseChartXml(xml, fallbackName) {
7509
7728
  type,
7510
7729
  title,
7511
7730
  categories: series.find((item) => item.categories.length > 0)?.categories || [],
7512
- series: series.map((item) => ({ name: item.name, values: item.values }))
7731
+ series: series.map((item) => ({ name: item.name, values: item.values, color: item.color }))
7513
7732
  };
7514
7733
  }
7734
+ function readChartTitle(doc, fallbackName) {
7735
+ const titleElement = Array.from(doc.getElementsByTagName("*")).find((element) => element.localName === "title");
7736
+ if (!titleElement) {
7737
+ return fallbackName.replace(/\.xml$/i, "");
7738
+ }
7739
+ const explicitTitle = chartText(titleElement);
7740
+ if (explicitTitle) {
7741
+ return explicitTitle;
7742
+ }
7743
+ const language = Array.from(doc.getElementsByTagName("*")).find((element) => element.localName === "lang")?.getAttribute("val") || "";
7744
+ return /^zh\b/i.test(language) ? "\u56FE\u8868\u6807\u9898" : "Chart Title";
7745
+ }
7515
7746
  function detectChartType(doc) {
7516
7747
  const chartType = Array.from(doc.getElementsByTagName("*")).find(
7517
7748
  (element) => element.localName.endsWith("Chart") && element.localName !== "chart"
@@ -7528,6 +7759,31 @@ function parseChartSeries(element, index) {
7528
7759
  categories: stringsFromFirst(element, "cat")
7529
7760
  };
7530
7761
  }
7762
+ function readChartSeriesColor(element) {
7763
+ const shape = Array.from(element?.children || []).find((child) => child.localName === "spPr");
7764
+ const color = Array.from(shape?.getElementsByTagName("*") || []).find(
7765
+ (child) => child.localName === "srgbClr" || child.localName === "schemeClr"
7766
+ );
7767
+ if (!color) {
7768
+ return void 0;
7769
+ }
7770
+ if (color.localName === "srgbClr") {
7771
+ const value = color.getAttribute("val") || "";
7772
+ return /^[\da-f]{6}$/i.test(value) ? `#${value}` : void 0;
7773
+ }
7774
+ return chartSchemeColor(color.getAttribute("val") || "");
7775
+ }
7776
+ function chartSchemeColor(value) {
7777
+ const colors = {
7778
+ accent1: "#156082",
7779
+ accent2: "#e97132",
7780
+ accent3: "#196b24",
7781
+ accent4: "#0f9ed5",
7782
+ accent5: "#a02b93",
7783
+ accent6: "#4ea72e"
7784
+ };
7785
+ return colors[value];
7786
+ }
7531
7787
  function renderChartPreviewSection(charts) {
7532
7788
  const section = createSection("\u8868\u683C\u56FE\u8868\u9884\u89C8");
7533
7789
  const grid = document.createElement("div");
@@ -7565,17 +7821,39 @@ function renderChartCard(chart) {
7565
7821
  }
7566
7822
  function renderChartSvg(chart) {
7567
7823
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
7568
- svg.setAttribute("viewBox", "0 0 640 260");
7824
+ svg.setAttribute("viewBox", "0 0 640 380");
7569
7825
  svg.setAttribute("role", "img");
7570
7826
  svg.setAttribute("aria-label", chart.title);
7571
7827
  svg.classList.add("ofv-chart-svg");
7572
7828
  const allValues = chart.series.flatMap((item) => item.values).filter((value) => Number.isFinite(value));
7573
7829
  const max = Math.max(1, ...allValues);
7574
7830
  const min = Math.min(0, ...allValues);
7831
+ const axisScale = createChartAxisScale(max, min);
7832
+ const axisMax = axisScale.max;
7833
+ const axisMin = axisScale.min;
7575
7834
  const span = max - min || 1;
7576
- const colors = ["#2563eb", "#059669", "#d97706", "#dc2626", "#7c3aed"];
7577
- const plot = { x: 48, y: 24, width: 552, height: 178 };
7578
- appendSvg(svg, "line", { x1: plot.x, y1: plot.y, x2: plot.x, y2: plot.y + plot.height, class: "ofv-chart-axis" });
7835
+ const colors = ["#156082", "#e97132", "#196b24", "#0f9ed5", "#a02b93", "#4ea72e"];
7836
+ const plot = { x: 74, y: 74, width: 526, height: 214 };
7837
+ const categories = chart.categories.length > 0 ? chart.categories : chart.series[0]?.values.map((_, index) => String(index + 1)) || [];
7838
+ const title = appendSvg(svg, "text", { x: 320, y: 34, class: "ofv-chart-title", "text-anchor": "middle" });
7839
+ title.textContent = chart.title;
7840
+ for (const value of axisScale.ticks) {
7841
+ const y = plot.y + plot.height - (value - axisMin) / (axisMax - axisMin || 1) * plot.height;
7842
+ appendSvg(svg, "line", {
7843
+ x1: plot.x,
7844
+ y1: Number(y.toFixed(1)),
7845
+ x2: plot.x + plot.width,
7846
+ y2: Number(y.toFixed(1)),
7847
+ class: value === 0 ? "ofv-chart-axis" : "ofv-chart-gridline"
7848
+ });
7849
+ const label = appendSvg(svg, "text", {
7850
+ x: plot.x - 12,
7851
+ y: Number((y + 4).toFixed(1)),
7852
+ class: "ofv-chart-label",
7853
+ "text-anchor": "end"
7854
+ });
7855
+ label.textContent = formatChartTick(value);
7856
+ }
7579
7857
  appendSvg(svg, "line", {
7580
7858
  x1: plot.x,
7581
7859
  y1: plot.y + plot.height,
@@ -7583,28 +7861,47 @@ function renderChartSvg(chart) {
7583
7861
  y2: plot.y + plot.height,
7584
7862
  class: "ofv-chart-axis"
7585
7863
  });
7586
- chart.series.forEach((series, seriesIndex) => {
7587
- const color = colors[seriesIndex % colors.length];
7588
- const step = series.values.length > 1 ? plot.width / (series.values.length - 1) : plot.width;
7589
- const points = series.values.map((value, index) => ({
7590
- x: plot.x + index * step,
7591
- y: plot.y + plot.height - (value - min) / span * plot.height
7592
- }));
7593
- if (chart.type.includes("bar") || chart.type.includes("col")) {
7594
- const barWidth = Math.max(4, Math.min(28, step * 0.6)) / Math.max(1, chart.series.length);
7595
- points.forEach((point, index) => {
7596
- const zeroY = plot.y + plot.height - (0 - min) / span * plot.height;
7597
- const x = point.x - barWidth * chart.series.length / 2 + seriesIndex * barWidth;
7864
+ if (chart.type.includes("bar") || chart.type.includes("col")) {
7865
+ const categoryCount = Math.max(1, categories.length, ...chart.series.map((series) => series.values.length));
7866
+ const groupWidth = plot.width / categoryCount;
7867
+ const clusterWidth = groupWidth * 0.58;
7868
+ const barWidth = Math.max(5, Math.min(28, clusterWidth / Math.max(1, chart.series.length)));
7869
+ const zeroY = plot.y + plot.height - (0 - axisMin) / (axisMax - axisMin || 1) * plot.height;
7870
+ categories.slice(0, categoryCount).forEach((category, index) => {
7871
+ const x = plot.x + groupWidth * (index + 0.5);
7872
+ const label = appendSvg(svg, "text", {
7873
+ x: Number(x.toFixed(1)),
7874
+ y: plot.y + plot.height + 22,
7875
+ class: "ofv-chart-label",
7876
+ "text-anchor": "middle"
7877
+ });
7878
+ label.textContent = truncateChartLabel(category);
7879
+ });
7880
+ chart.series.forEach((series, seriesIndex) => {
7881
+ const color = series.color || colors[seriesIndex % colors.length];
7882
+ series.values.forEach((value, index) => {
7883
+ const groupCenter = plot.x + groupWidth * (index + 0.5);
7884
+ const x = groupCenter - barWidth * chart.series.length / 2 + seriesIndex * barWidth + barWidth * 0.12;
7885
+ const y = plot.y + plot.height - (value - axisMin) / (axisMax - axisMin || 1) * plot.height;
7598
7886
  appendSvg(svg, "rect", {
7599
- x,
7600
- y: Math.min(point.y, zeroY),
7601
- width: barWidth,
7602
- height: Math.max(1, Math.abs(zeroY - point.y)),
7887
+ x: Number(x.toFixed(1)),
7888
+ y: Number(Math.min(y, zeroY).toFixed(1)),
7889
+ width: Number((barWidth * 0.76).toFixed(1)),
7890
+ height: Number(Math.max(1, Math.abs(zeroY - y)).toFixed(1)),
7603
7891
  fill: color,
7604
7892
  "data-index": index
7605
7893
  });
7606
7894
  });
7607
- } else {
7895
+ });
7896
+ } else {
7897
+ chart.series.forEach((series, seriesIndex) => {
7898
+ const color = series.color || colors[seriesIndex % colors.length];
7899
+ const step = series.values.length > 1 ? plot.width / (series.values.length - 1) : plot.width;
7900
+ const lineSpan = axisMax - axisMin || span;
7901
+ const points = series.values.map((value, index) => ({
7902
+ x: plot.x + index * step,
7903
+ y: plot.y + plot.height - (value - axisMin) / lineSpan * plot.height
7904
+ }));
7608
7905
  appendSvg(svg, "polyline", {
7609
7906
  points: points.map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(" "),
7610
7907
  fill: "none",
@@ -7616,11 +7913,49 @@ function renderChartSvg(chart) {
7616
7913
  for (const point of points.slice(0, 80)) {
7617
7914
  appendSvg(svg, "circle", { cx: point.x, cy: point.y, r: 3, fill: color });
7618
7915
  }
7619
- }
7620
- appendLegend(svg, series.name, color, 52 + seriesIndex * 118, 236);
7621
- });
7916
+ });
7917
+ }
7918
+ appendChartLegend(svg, chart, colors, 348);
7622
7919
  return svg;
7623
7920
  }
7921
+ function appendChartLegend(svg, chart, colors, y) {
7922
+ const itemWidth = 86;
7923
+ const startX = 320 - chart.series.length * itemWidth / 2;
7924
+ chart.series.forEach((series, seriesIndex) => {
7925
+ const color = series.color || colors[seriesIndex % colors.length];
7926
+ appendLegend(svg, series.name, color, startX + seriesIndex * itemWidth, y);
7927
+ });
7928
+ }
7929
+ function createChartAxisScale(max, min) {
7930
+ const axisMin = Math.min(0, min);
7931
+ const positiveMax = Math.max(1, max);
7932
+ const step = niceChartStep((positiveMax - axisMin) / 5);
7933
+ let axisMax = Math.ceil(positiveMax / step) * step;
7934
+ if (axisMax <= positiveMax) {
7935
+ axisMax += step;
7936
+ }
7937
+ const ticks = [];
7938
+ for (let value = axisMin; value <= axisMax + step / 2; value += step) {
7939
+ ticks.push(Number(value.toFixed(6)));
7940
+ }
7941
+ return { min: axisMin, max: axisMax, ticks };
7942
+ }
7943
+ function niceChartStep(rawStep) {
7944
+ if (rawStep <= 0) {
7945
+ return 1;
7946
+ }
7947
+ const magnitude = 10 ** Math.floor(Math.log10(rawStep));
7948
+ const normalized = rawStep / magnitude;
7949
+ const nice = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
7950
+ return nice * magnitude;
7951
+ }
7952
+ function formatChartTick(value) {
7953
+ const rounded = Math.abs(value) < 1 ? Number(value.toFixed(1)) : Number(value.toFixed(0));
7954
+ return String(rounded);
7955
+ }
7956
+ function truncateChartLabel(value) {
7957
+ return value.length > 10 ? `${value.slice(0, 10)}...` : value;
7958
+ }
7624
7959
  function appendLegend(svg, label, color, x, y) {
7625
7960
  appendSvg(svg, "rect", { x, y: y - 10, width: 12, height: 12, rx: 2, fill: color });
7626
7961
  const text = appendSvg(svg, "text", { x: x + 18, y, class: "ofv-chart-label" });
@@ -8354,7 +8689,7 @@ function renderOpenDocumentPresentationXml(panel, xml) {
8354
8689
  renderPresentationInsight(panel, inspectOpenDocumentPresentation("FODP \u6F14\u793A\u6587\u7A3F", xml, 0));
8355
8690
  renderOpenDocumentPresentation(panel, "FODP \u6F14\u793A\u6587\u7A3F", xml, []);
8356
8691
  }
8357
- async function renderPackagedOfficePreview(panel, arrayBuffer, extension) {
8692
+ async function renderPackagedOfficePreview(panel, arrayBuffer, extension, fit) {
8358
8693
  let zip;
8359
8694
  try {
8360
8695
  zip = await JSZip3.loadAsync(arrayBuffer);
@@ -8365,7 +8700,7 @@ async function renderPackagedOfficePreview(panel, arrayBuffer, extension) {
8365
8700
  const hasEntry = (path) => entries.some((entry) => entry.name.toLowerCase() === path.toLowerCase());
8366
8701
  const contentXml = zip.file(/(^|\/)content\.xml$/i)[0];
8367
8702
  if (hasEntry("word/document.xml")) {
8368
- await renderDocx(panel, arrayBuffer);
8703
+ await renderDocx(panel, arrayBuffer, fit);
8369
8704
  return true;
8370
8705
  }
8371
8706
  if (hasEntry("xl/workbook.xml")) {
@@ -18817,7 +19152,7 @@ async function createPostScriptPreview(bytes, url, fileName, size, fit, toolbar)
18817
19152
  preview.append(summary);
18818
19153
  return { element: preview };
18819
19154
  }
18820
- async function createPdfCompatibleAiPreview(bytes, url, fileName, size, fit, toolbar, pdfOffset = 0) {
19155
+ async function createPdfCompatibleAiPreview(bytes, url, fileName, size, fit, toolbar, pdfOffset = 0, zoom = 1) {
18821
19156
  const wrapper = document.createElement("div");
18822
19157
  wrapper.className = "ofv-ai-pdf-preview";
18823
19158
  let pdfUrl = url;
@@ -18832,6 +19167,7 @@ async function createPdfCompatibleAiPreview(bytes, url, fileName, size, fit, too
18832
19167
  viewport: wrapper,
18833
19168
  size,
18834
19169
  fit,
19170
+ zoom,
18835
19171
  toolbar,
18836
19172
  fallbackTitle: "AI PDF \u517C\u5BB9\u9884\u89C8\u5931\u8D25",
18837
19173
  revokeUrlOnDestroy: false