@open-file-viewer/core 0.1.15 → 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
@@ -1877,12 +1877,12 @@ function imagePlugin() {
1877
1877
  let url = "";
1878
1878
  let convertedBlob = null;
1879
1879
  let isExternal = Boolean(ctx.file.url);
1880
- let canvasSource = null;
1880
+ let tiffSource = null;
1881
1881
  if (isTiff) {
1882
1882
  ctx.setLoading(true);
1883
1883
  try {
1884
1884
  const bytes = await sourceBytesPromise;
1885
- canvasSource = await createTiffCanvas(bytes);
1885
+ tiffSource = await createTiffPreview(bytes, ctx.file.name);
1886
1886
  } catch (err) {
1887
1887
  console.error("TIFF image conversion failed:", err);
1888
1888
  url = createObjectUrl(ctx.file);
@@ -1942,12 +1942,15 @@ function imagePlugin() {
1942
1942
  if (url) {
1943
1943
  image.src = url;
1944
1944
  }
1945
- const visual = canvasSource || image;
1946
- if (canvasSource) {
1947
- canvasSource.classList.add("ofv-media", "ofv-image-content", "ofv-tiff-canvas");
1948
- canvasSource.setAttribute("role", "img");
1949
- 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
+ }
1950
1951
  }
1952
+ const visualBox = document.createElement("div");
1953
+ visualBox.className = "ofv-image-scrollbox";
1951
1954
  let scale = getInitialZoom(ctx);
1952
1955
  let rotation = 0;
1953
1956
  let offsetX = 0;
@@ -1961,7 +1964,23 @@ function imagePlugin() {
1961
1964
  let previewAvailable = true;
1962
1965
  const zoomLabel = document.createElement("span");
1963
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
+ };
1964
1982
  const updateTransform = () => {
1983
+ updateScrollBox();
1965
1984
  visual.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale}) rotate(${rotation}deg)`;
1966
1985
  zoomLabel.textContent = `${Math.round(scale * 100)}%`;
1967
1986
  ctx.toolbar?.setZoom(previewAvailable ? scale : void 0);
@@ -2081,11 +2100,13 @@ function imagePlugin() {
2081
2100
  stage.addEventListener("lostpointercapture", onLostPointerCapture);
2082
2101
  stage.addEventListener("pointerleave", onPointerLeave);
2083
2102
  stage.addEventListener("wheel", onWheel, { passive: false });
2084
- if (!canvasSource) {
2103
+ if (!tiffSource) {
2085
2104
  image.addEventListener("error", showImageFallback);
2105
+ image.addEventListener("load", updateScrollBox);
2086
2106
  }
2087
2107
  window.addEventListener("blur", onWindowBlur);
2088
- stage.append(visual);
2108
+ visualBox.append(visual);
2109
+ stage.append(visualBox);
2089
2110
  wrapper.append(...showInlineControls ? [controls, stage, infoBar] : [stage, infoBar]);
2090
2111
  ctx.viewport.append(wrapper);
2091
2112
  updateTransform();
@@ -2123,7 +2144,12 @@ function imagePlugin() {
2123
2144
  },
2124
2145
  resize(size) {
2125
2146
  visual.style.maxWidth = `${size.width}px`;
2126
- 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();
2127
2153
  },
2128
2154
  destroy() {
2129
2155
  ctx.toolbar?.setZoom(void 0);
@@ -2138,6 +2164,7 @@ function imagePlugin() {
2138
2164
  stage.removeEventListener("pointerleave", onPointerLeave);
2139
2165
  stage.removeEventListener("wheel", onWheel);
2140
2166
  image.removeEventListener("error", showImageFallback);
2167
+ image.removeEventListener("load", updateScrollBox);
2141
2168
  window.removeEventListener("blur", onWindowBlur);
2142
2169
  finishDrag();
2143
2170
  wrapper.remove();
@@ -2151,33 +2178,64 @@ function imagePlugin() {
2151
2178
  }
2152
2179
  };
2153
2180
  }
2154
- async function createTiffCanvas(bytes) {
2181
+ async function createTiffPreview(bytes, fileName) {
2155
2182
  if (bytes.byteLength === 0) {
2156
2183
  throw new Error("\u65E0\u6CD5\u8BFB\u53D6 TIFF \u6587\u4EF6\u5185\u5BB9\u3002");
2157
2184
  }
2158
2185
  const UTIF = await import("utif");
2159
2186
  const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
2160
2187
  const ifds = UTIF.decode(buffer);
2161
- const ifd = ifds.find((item) => Number(item.width || 0) > 0 || Number(item.t256 || 0) > 0) || ifds[0];
2162
- if (!ifd) {
2188
+ const imageIfds = ifds.filter((item) => hasTiffDimensions(item));
2189
+ if (imageIfds.length === 0) {
2163
2190
  throw new Error("TIFF \u6587\u4EF6\u6CA1\u6709\u53EF\u89E3\u7801\u7684\u56FE\u50CF\u76EE\u5F55\u3002");
2164
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) {
2165
2211
  UTIF.decodeImage(buffer, ifd);
2166
2212
  const rgba = UTIF.toRGBA8(ifd);
2167
- const width = Number(ifd.width || ifd.t256 || 0);
2168
- const height = Number(ifd.height || ifd.t257 || 0);
2213
+ const { width, height } = getTiffDimensions(ifd);
2169
2214
  if (!width || !height || rgba.length < width * height * 4) {
2170
2215
  throw new Error("TIFF \u56FE\u50CF\u50CF\u7D20\u6570\u636E\u4E0D\u5B8C\u6574\u3002");
2171
2216
  }
2172
2217
  const canvas = document.createElement("canvas");
2218
+ canvas.className = "ofv-tiff-canvas";
2173
2219
  canvas.width = width;
2174
2220
  canvas.height = height;
2221
+ canvas.setAttribute("role", "img");
2222
+ canvas.setAttribute("aria-label", total > 1 ? `${fileName} \u7B2C ${index + 1} \u9875` : fileName);
2175
2223
  const context = canvas.getContext("2d");
2176
2224
  if (!context) {
2177
2225
  throw new Error("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301 Canvas 2D\uFF0C\u65E0\u6CD5\u5C55\u793A TIFF\u3002");
2178
2226
  }
2179
2227
  context.putImageData(new ImageData(new Uint8ClampedArray(rgba), width, height), 0, 0);
2180
- 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
+ };
2181
2239
  }
2182
2240
  function createImageFallback(fileName, url) {
2183
2241
  const fallback = document.createElement("div");
@@ -2506,6 +2564,7 @@ function parseTiffInfo(bytes) {
2506
2564
  return { format: magic === 43 ? "BigTIFF" : "TIFF", note: "IFD \u504F\u79FB\u8D85\u51FA\u6587\u4EF6\u8303\u56F4" };
2507
2565
  }
2508
2566
  const count = magic === 43 ? readTiffUint64AsNumber(bytes, ifdOffset, littleEndian) : readTiffUint16(bytes, ifdOffset, littleEndian);
2567
+ const imageCount = countTiffIfds(bytes, ifdOffset, magic === 43, littleEndian);
2509
2568
  const entrySize = magic === 43 ? 20 : 12;
2510
2569
  const entriesStart = ifdOffset + (magic === 43 ? 8 : 2);
2511
2570
  let width;
@@ -2532,9 +2591,26 @@ function parseTiffInfo(bytes) {
2532
2591
  height,
2533
2592
  bitDepth: bitDepth ? `${bitDepth} bit` : void 0,
2534
2593
  color: compression ? tiffCompressionName(compression) : void 0,
2535
- count: Number.isFinite(count) ? count : void 0
2594
+ count: imageCount || void 0
2536
2595
  };
2537
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
+ }
2538
2614
  function readTiffInlineValue(bytes, offset, type, littleEndian) {
2539
2615
  if (offset + 4 > bytes.length) {
2540
2616
  return void 0;
@@ -3933,26 +4009,6 @@ var mimeLangMap = {
3933
4009
  };
3934
4010
  var MAX_HIGHLIGHT_CHARS = 18e4;
3935
4011
  var MAX_RENDER_CHARS = 6e5;
3936
- function loadPrismCss(theme) {
3937
- const lightId = "ofv-prism-css-light";
3938
- const darkId = "ofv-prism-css-dark";
3939
- const activeId = theme === "dark" ? darkId : lightId;
3940
- const inactiveId = theme === "dark" ? lightId : darkId;
3941
- document.getElementById(inactiveId)?.remove();
3942
- if (document.getElementById(activeId)) {
3943
- return Promise.resolve();
3944
- }
3945
- 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";
3946
- return new Promise((resolve, reject) => {
3947
- const link = document.createElement("link");
3948
- link.id = activeId;
3949
- link.rel = "stylesheet";
3950
- link.href = href;
3951
- link.onload = () => resolve();
3952
- link.onerror = () => reject(new Error(`Failed to load Prism CSS: ${href}`));
3953
- document.head.appendChild(link);
3954
- });
3955
- }
3956
4012
  function textPlugin() {
3957
4013
  return {
3958
4014
  name: "text",
@@ -3976,7 +4032,6 @@ function textPlugin() {
3976
4032
  }
3977
4033
  };
3978
4034
  }
3979
- 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";
3980
4035
  if (isMarkdown) {
3981
4036
  const [markedModule, PrismModule2, DOMPurifyModule] = await Promise.all([
3982
4037
  import("marked"),
@@ -3997,7 +4052,6 @@ function textPlugin() {
3997
4052
  try {
3998
4053
  const codeBlocks = container.querySelectorAll("pre code");
3999
4054
  if (codeBlocks.length > 0) {
4000
- await loadPrismCss(isDark ? "dark" : "light");
4001
4055
  codeBlocks.forEach((block) => {
4002
4056
  const parent = block.parentElement;
4003
4057
  if (parent && !parent.className.includes("language-")) {
@@ -4121,9 +4175,6 @@ function textPlugin() {
4121
4175
  console.warn(`Prism failed to load language component for: ${lang}`, e);
4122
4176
  }
4123
4177
  }
4124
- await loadPrismCss(isDark ? "dark" : "light").catch((error) => {
4125
- console.warn("Prism CSS failed to load; rendering code without external theme:", error);
4126
- });
4127
4178
  const codeText = text.length > MAX_RENDER_CHARS ? text.slice(0, MAX_RENDER_CHARS) : text;
4128
4179
  const totalLines = countLines(text);
4129
4180
  const shownLines = countLines(codeText);
@@ -4651,10 +4702,11 @@ async function renderPdfDocumentPreview(options) {
4651
4702
  const baseViewport = page.getViewport({ scale: 1 });
4652
4703
  pagesMeta.push({
4653
4704
  width: baseViewport.width,
4654
- height: baseViewport.height
4705
+ height: baseViewport.height,
4706
+ rotation: getPdfPageRotation(page)
4655
4707
  });
4656
4708
  } catch {
4657
- pagesMeta.push({ width: 612, height: 792 });
4709
+ pagesMeta.push({ width: 612, height: 792, rotation: 0 });
4658
4710
  }
4659
4711
  }
4660
4712
  const pageStates = [];
@@ -4689,7 +4741,7 @@ async function renderPdfDocumentPreview(options) {
4689
4741
  const page = await pdfDocument.getPage(pageIdx + 1);
4690
4742
  const meta = pagesMeta[pageIdx];
4691
4743
  const scale = options.fit === "actual" ? zoomFactor : Math.max(0.05, Math.min(5, getPdfAvailableWidth(size.width) / rotatedPdfWidth(meta, rotation) * zoomFactor));
4692
- const viewport = page.getViewport({ scale, rotation });
4744
+ const viewport = page.getViewport({ scale, rotation: getPdfRenderRotation(meta, rotation) });
4693
4745
  const outputScale = getPdfOutputScale();
4694
4746
  const cssWidth = Math.floor(viewport.width);
4695
4747
  const cssHeight = Math.floor(viewport.height);
@@ -4728,6 +4780,8 @@ async function renderPdfDocumentPreview(options) {
4728
4780
  const span = document.createElement("span");
4729
4781
  span.textContent = str;
4730
4782
  span.style.fontSize = `${fontHeight}px`;
4783
+ span.style.lineHeight = "1";
4784
+ span.style.height = `${fontHeight}px`;
4731
4785
  span.style.fontFamily = item.fontName || "sans-serif";
4732
4786
  span.style.left = `${tx[4]}px`;
4733
4787
  span.style.top = `${tx[5] - fontHeight}px`;
@@ -4925,6 +4979,13 @@ function appendPdfSummary(parent, label, value) {
4925
4979
  function normalizePdfRotation(value) {
4926
4980
  return (value % 360 + 360) % 360;
4927
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
+ }
4928
4989
  function isPdfRotatedSideways(rotation) {
4929
4990
  const normalized = normalizePdfRotation(rotation);
4930
4991
  return normalized === 90 || normalized === 270;
@@ -5895,13 +5956,13 @@ function officePlugin(options = {}) {
5895
5956
  if (conversionContext && await shouldUseOfficeConversion(options, conversionContext)) {
5896
5957
  delegatedInstance = await renderConvertedOfficePreview(panel, ctx, options, conversionContext);
5897
5958
  } else if (packageFormat === "docx" && !fileIsDocx(extension)) {
5898
- disposeDocxFit = await renderDocx(panel, arrayBuffer);
5959
+ disposeDocxFit = await renderDocx(panel, arrayBuffer, ctx.options.fit);
5899
5960
  } else if (packageFormat === "xlsx" && !sheetExtensions.has(extension)) {
5900
5961
  await renderSheet(panel, arrayBuffer, "xlsx");
5901
5962
  } else if (packageFormat === "pptx" && !["pptx", "pptm", "ppsx", "ppsm", "potx", "potm"].includes(extension)) {
5902
5963
  await renderPptx(panel, arrayBuffer);
5903
5964
  } else if (fileIsDocx(extension)) {
5904
- disposeDocxFit = await renderDocx(panel, arrayBuffer);
5965
+ disposeDocxFit = await renderDocx(panel, arrayBuffer, ctx.options.fit);
5905
5966
  } else if (extension === "rtf") {
5906
5967
  renderPlainDocument(panel, "RTF \u6587\u6863", rtfToText(await readTextFromBuffer(arrayBuffer)));
5907
5968
  } else if (extension === "odt") {
@@ -5910,7 +5971,7 @@ function officePlugin(options = {}) {
5910
5971
  renderOpenDocumentXml(panel, "FODT \u6587\u6863", await readTextFromBuffer(arrayBuffer));
5911
5972
  } else if (extension === "fods") {
5912
5973
  renderFlatOds(panel, await readTextFromBuffer(arrayBuffer));
5913
- } else if (packagedOfficeCandidates.has(extension) && await renderPackagedOfficePreview(panel, arrayBuffer, extension)) {
5974
+ } else if (packagedOfficeCandidates.has(extension) && await renderPackagedOfficePreview(panel, arrayBuffer, extension, ctx.options.fit)) {
5914
5975
  } else if (sheetExtensions.has(extension)) {
5915
5976
  await renderSheet(panel, arrayBuffer, extension);
5916
5977
  } else if (["pptx", "pptm", "ppsx", "ppsm", "potx", "potm"].includes(extension)) {
@@ -6112,7 +6173,7 @@ async function detectPackagedOfficeFormat(arrayBuffer) {
6112
6173
  }
6113
6174
  return void 0;
6114
6175
  }
6115
- async function renderDocx(panel, arrayBuffer) {
6176
+ async function renderDocx(panel, arrayBuffer, fit) {
6116
6177
  const content = document.createElement("div");
6117
6178
  content.className = "ofv-docx-document";
6118
6179
  const styleContainer = document.createElement("div");
@@ -6151,7 +6212,7 @@ async function renderDocx(panel, arrayBuffer) {
6151
6212
  return () => void 0;
6152
6213
  }
6153
6214
  panel.append(content);
6154
- disposeFit = fitDocxPages(content);
6215
+ disposeFit = fitDocxPages(content, fit);
6155
6216
  return () => {
6156
6217
  disposeFit?.();
6157
6218
  styleContainer.remove();
@@ -6749,7 +6810,8 @@ function looksLikeDocxTextboxHeading(value) {
6749
6810
  return text.length > 0 && text.length <= 12 && !/[0-9@.::]/.test(text);
6750
6811
  }
6751
6812
  async function normalizeDocxLayout(container, arrayBuffer) {
6752
- const hints = await readDocxLayoutHints(arrayBuffer);
6813
+ const [hints, charts] = await Promise.all([readDocxLayoutHints(arrayBuffer), readDocxCharts(arrayBuffer)]);
6814
+ repairDocxChartPlaceholders(container, charts);
6753
6815
  const pages = container.querySelectorAll("section.ofv-docx");
6754
6816
  for (const page of pages) {
6755
6817
  repairDocxShapeFills(page);
@@ -6764,6 +6826,75 @@ async function normalizeDocxLayout(container, arrayBuffer) {
6764
6826
  }
6765
6827
  }
6766
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
+ }
6767
6898
  function repairDocxHeadingShapeAlignment(page) {
6768
6899
  for (const paragraph of page.querySelectorAll("p")) {
6769
6900
  const text = normalizePreviewText(paragraph.textContent || "");
@@ -6914,14 +7045,19 @@ function repairDocxShapeFills(page) {
6914
7045
  }
6915
7046
  }
6916
7047
  function repairDocxFloatingPictures(page, hints) {
6917
- const hint = hints.floatingPictures.find((item) => item.relativeFrom === "column" && item.wrap === "square");
6918
- if (!hint) {
7048
+ const pageHints = hints.floatingPictures.filter((item) => item.relativeFrom === "column" && item.wrap === "square");
7049
+ if (pageHints.length === 0) {
6919
7050
  return;
6920
7051
  }
6921
- const image = page.querySelector("img");
6922
- 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) {
6923
7056
  return;
6924
7057
  }
7058
+ images.forEach((image, index) => repairDocxFloatingPicture(page, image, pageHints[index]));
7059
+ }
7060
+ function repairDocxFloatingPicture(page, image, hint) {
6925
7061
  const wrapper = image.parentElement;
6926
7062
  if (!wrapper || wrapper.dataset.ofvDocxFloatRepaired === "true") {
6927
7063
  return;
@@ -6967,7 +7103,7 @@ function parseCssLineHeight(value) {
6967
7103
  const parsed = Number.parseFloat(trimmed);
6968
7104
  return Number.isFinite(parsed) ? parsed : 0;
6969
7105
  }
6970
- function fitDocxPages(container) {
7106
+ function fitDocxPages(container, fit) {
6971
7107
  const wrapper = container.querySelector(".ofv-docx-wrapper");
6972
7108
  if (!wrapper) {
6973
7109
  return () => void 0;
@@ -6980,6 +7116,7 @@ function fitDocxPages(container) {
6980
7116
  return;
6981
7117
  }
6982
7118
  const availableWidth = Math.max(1, container.clientWidth - 48);
7119
+ const availableHeight = container.clientHeight > 0 ? Math.max(1, container.clientHeight - 48) : void 0;
6983
7120
  const pageWidth = Math.max(
6984
7121
  1,
6985
7122
  ...frames.map(({ page }) => {
@@ -6987,14 +7124,24 @@ function fitDocxPages(container) {
6987
7124
  return page.offsetWidth || rectWidth || parseCssPixelValue(page.style.width) || 794;
6988
7125
  })
6989
7126
  );
6990
- 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);
6991
7132
  const userZoom = parseCssPixelValue(panel?.style.getPropertyValue("--ofv-office-zoom") || "1") || 1;
6992
7133
  wrapper.style.setProperty("--ofv-docx-scale", formatCssNumber(scale));
6993
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";
6994
7138
  for (const { frame, page } of frames) {
6995
- const pageHeight = page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height);
6996
- if (pageHeight > 0) {
6997
- 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`;
6998
7145
  }
6999
7146
  }
7000
7147
  };
@@ -7019,6 +7166,26 @@ function fitDocxPages(container) {
7019
7166
  observer.disconnect();
7020
7167
  };
7021
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
+ }
7022
7189
  function ensureDocxPageFrames(wrapper) {
7023
7190
  const pages = Array.from(wrapper.querySelectorAll("section.ofv-docx"));
7024
7191
  return pages.map((page) => {
@@ -7550,9 +7717,9 @@ function parseChartXml(xml, fallbackName) {
7550
7717
  return null;
7551
7718
  }
7552
7719
  const type = detectChartType(doc);
7553
- const title = textFromFirst(doc, "title") || fallbackName.replace(/\.xml$/i, "");
7720
+ const title = readChartTitle(doc, fallbackName);
7554
7721
  const seriesElements = Array.from(doc.getElementsByTagName("*")).filter((element) => element.localName === "ser");
7555
- 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);
7556
7723
  if (series.length === 0) {
7557
7724
  return null;
7558
7725
  }
@@ -7561,9 +7728,21 @@ function parseChartXml(xml, fallbackName) {
7561
7728
  type,
7562
7729
  title,
7563
7730
  categories: series.find((item) => item.categories.length > 0)?.categories || [],
7564
- series: series.map((item) => ({ name: item.name, values: item.values }))
7731
+ series: series.map((item) => ({ name: item.name, values: item.values, color: item.color }))
7565
7732
  };
7566
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
+ }
7567
7746
  function detectChartType(doc) {
7568
7747
  const chartType = Array.from(doc.getElementsByTagName("*")).find(
7569
7748
  (element) => element.localName.endsWith("Chart") && element.localName !== "chart"
@@ -7580,6 +7759,31 @@ function parseChartSeries(element, index) {
7580
7759
  categories: stringsFromFirst(element, "cat")
7581
7760
  };
7582
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
+ }
7583
7787
  function renderChartPreviewSection(charts) {
7584
7788
  const section = createSection("\u8868\u683C\u56FE\u8868\u9884\u89C8");
7585
7789
  const grid = document.createElement("div");
@@ -7617,17 +7821,39 @@ function renderChartCard(chart) {
7617
7821
  }
7618
7822
  function renderChartSvg(chart) {
7619
7823
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
7620
- svg.setAttribute("viewBox", "0 0 640 260");
7824
+ svg.setAttribute("viewBox", "0 0 640 380");
7621
7825
  svg.setAttribute("role", "img");
7622
7826
  svg.setAttribute("aria-label", chart.title);
7623
7827
  svg.classList.add("ofv-chart-svg");
7624
7828
  const allValues = chart.series.flatMap((item) => item.values).filter((value) => Number.isFinite(value));
7625
7829
  const max = Math.max(1, ...allValues);
7626
7830
  const min = Math.min(0, ...allValues);
7831
+ const axisScale = createChartAxisScale(max, min);
7832
+ const axisMax = axisScale.max;
7833
+ const axisMin = axisScale.min;
7627
7834
  const span = max - min || 1;
7628
- const colors = ["#2563eb", "#059669", "#d97706", "#dc2626", "#7c3aed"];
7629
- const plot = { x: 48, y: 24, width: 552, height: 178 };
7630
- 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
+ }
7631
7857
  appendSvg(svg, "line", {
7632
7858
  x1: plot.x,
7633
7859
  y1: plot.y + plot.height,
@@ -7635,28 +7861,47 @@ function renderChartSvg(chart) {
7635
7861
  y2: plot.y + plot.height,
7636
7862
  class: "ofv-chart-axis"
7637
7863
  });
7638
- chart.series.forEach((series, seriesIndex) => {
7639
- const color = colors[seriesIndex % colors.length];
7640
- const step = series.values.length > 1 ? plot.width / (series.values.length - 1) : plot.width;
7641
- const points = series.values.map((value, index) => ({
7642
- x: plot.x + index * step,
7643
- y: plot.y + plot.height - (value - min) / span * plot.height
7644
- }));
7645
- if (chart.type.includes("bar") || chart.type.includes("col")) {
7646
- const barWidth = Math.max(4, Math.min(28, step * 0.6)) / Math.max(1, chart.series.length);
7647
- points.forEach((point, index) => {
7648
- const zeroY = plot.y + plot.height - (0 - min) / span * plot.height;
7649
- 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;
7650
7886
  appendSvg(svg, "rect", {
7651
- x,
7652
- y: Math.min(point.y, zeroY),
7653
- width: barWidth,
7654
- 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)),
7655
7891
  fill: color,
7656
7892
  "data-index": index
7657
7893
  });
7658
7894
  });
7659
- } 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
+ }));
7660
7905
  appendSvg(svg, "polyline", {
7661
7906
  points: points.map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(" "),
7662
7907
  fill: "none",
@@ -7668,11 +7913,49 @@ function renderChartSvg(chart) {
7668
7913
  for (const point of points.slice(0, 80)) {
7669
7914
  appendSvg(svg, "circle", { cx: point.x, cy: point.y, r: 3, fill: color });
7670
7915
  }
7671
- }
7672
- appendLegend(svg, series.name, color, 52 + seriesIndex * 118, 236);
7673
- });
7916
+ });
7917
+ }
7918
+ appendChartLegend(svg, chart, colors, 348);
7674
7919
  return svg;
7675
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
+ }
7676
7959
  function appendLegend(svg, label, color, x, y) {
7677
7960
  appendSvg(svg, "rect", { x, y: y - 10, width: 12, height: 12, rx: 2, fill: color });
7678
7961
  const text = appendSvg(svg, "text", { x: x + 18, y, class: "ofv-chart-label" });
@@ -8406,7 +8689,7 @@ function renderOpenDocumentPresentationXml(panel, xml) {
8406
8689
  renderPresentationInsight(panel, inspectOpenDocumentPresentation("FODP \u6F14\u793A\u6587\u7A3F", xml, 0));
8407
8690
  renderOpenDocumentPresentation(panel, "FODP \u6F14\u793A\u6587\u7A3F", xml, []);
8408
8691
  }
8409
- async function renderPackagedOfficePreview(panel, arrayBuffer, extension) {
8692
+ async function renderPackagedOfficePreview(panel, arrayBuffer, extension, fit) {
8410
8693
  let zip;
8411
8694
  try {
8412
8695
  zip = await JSZip3.loadAsync(arrayBuffer);
@@ -8417,7 +8700,7 @@ async function renderPackagedOfficePreview(panel, arrayBuffer, extension) {
8417
8700
  const hasEntry = (path) => entries.some((entry) => entry.name.toLowerCase() === path.toLowerCase());
8418
8701
  const contentXml = zip.file(/(^|\/)content\.xml$/i)[0];
8419
8702
  if (hasEntry("word/document.xml")) {
8420
- await renderDocx(panel, arrayBuffer);
8703
+ await renderDocx(panel, arrayBuffer, fit);
8421
8704
  return true;
8422
8705
  }
8423
8706
  if (hasEntry("xl/workbook.xml")) {