@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.cjs CHANGED
@@ -1930,12 +1930,12 @@ function imagePlugin() {
1930
1930
  let url = "";
1931
1931
  let convertedBlob = null;
1932
1932
  let isExternal = Boolean(ctx.file.url);
1933
- let canvasSource = null;
1933
+ let tiffSource = null;
1934
1934
  if (isTiff) {
1935
1935
  ctx.setLoading(true);
1936
1936
  try {
1937
1937
  const bytes = await sourceBytesPromise;
1938
- canvasSource = await createTiffCanvas(bytes);
1938
+ tiffSource = await createTiffPreview(bytes, ctx.file.name);
1939
1939
  } catch (err) {
1940
1940
  console.error("TIFF image conversion failed:", err);
1941
1941
  url = createObjectUrl(ctx.file);
@@ -1995,12 +1995,15 @@ function imagePlugin() {
1995
1995
  if (url) {
1996
1996
  image.src = url;
1997
1997
  }
1998
- const visual = canvasSource || image;
1999
- if (canvasSource) {
2000
- canvasSource.classList.add("ofv-media", "ofv-image-content", "ofv-tiff-canvas");
2001
- canvasSource.setAttribute("role", "img");
2002
- canvasSource.setAttribute("aria-label", ctx.file.name);
1998
+ const visual = tiffSource || image;
1999
+ if (tiffSource) {
2000
+ tiffSource.classList.add("ofv-image-content");
2001
+ if (tiffSource.classList.contains("ofv-tiff-pages")) {
2002
+ stage.classList.add("ofv-image-stage-pages");
2003
+ }
2003
2004
  }
2005
+ const visualBox = document.createElement("div");
2006
+ visualBox.className = "ofv-image-scrollbox";
2004
2007
  let scale = getInitialZoom(ctx);
2005
2008
  let rotation = 0;
2006
2009
  let offsetX = 0;
@@ -2014,7 +2017,23 @@ function imagePlugin() {
2014
2017
  let previewAvailable = true;
2015
2018
  const zoomLabel = document.createElement("span");
2016
2019
  zoomLabel.className = "ofv-image-zoom";
2020
+ const updateScrollBox = () => {
2021
+ if (visual.classList.contains("ofv-tiff-pages")) {
2022
+ visualBox.style.removeProperty("width");
2023
+ visualBox.style.removeProperty("height");
2024
+ return;
2025
+ }
2026
+ const baseWidth = visual.offsetWidth || visual.getBoundingClientRect().width || visual.naturalWidth || 0;
2027
+ const baseHeight = visual.offsetHeight || visual.getBoundingClientRect().height || visual.naturalHeight || 0;
2028
+ if (baseWidth > 0) {
2029
+ visualBox.style.width = `${Math.ceil(baseWidth * scale)}px`;
2030
+ }
2031
+ if (baseHeight > 0) {
2032
+ visualBox.style.height = `${Math.ceil(baseHeight * scale)}px`;
2033
+ }
2034
+ };
2017
2035
  const updateTransform = () => {
2036
+ updateScrollBox();
2018
2037
  visual.style.transform = `translate(${offsetX}px, ${offsetY}px) scale(${scale}) rotate(${rotation}deg)`;
2019
2038
  zoomLabel.textContent = `${Math.round(scale * 100)}%`;
2020
2039
  ctx.toolbar?.setZoom(previewAvailable ? scale : void 0);
@@ -2134,11 +2153,13 @@ function imagePlugin() {
2134
2153
  stage.addEventListener("lostpointercapture", onLostPointerCapture);
2135
2154
  stage.addEventListener("pointerleave", onPointerLeave);
2136
2155
  stage.addEventListener("wheel", onWheel, { passive: false });
2137
- if (!canvasSource) {
2156
+ if (!tiffSource) {
2138
2157
  image.addEventListener("error", showImageFallback);
2158
+ image.addEventListener("load", updateScrollBox);
2139
2159
  }
2140
2160
  window.addEventListener("blur", onWindowBlur);
2141
- stage.append(visual);
2161
+ visualBox.append(visual);
2162
+ stage.append(visualBox);
2142
2163
  wrapper.append(...showInlineControls ? [controls, stage, infoBar] : [stage, infoBar]);
2143
2164
  ctx.viewport.append(wrapper);
2144
2165
  updateTransform();
@@ -2176,7 +2197,12 @@ function imagePlugin() {
2176
2197
  },
2177
2198
  resize(size) {
2178
2199
  visual.style.maxWidth = `${size.width}px`;
2179
- visual.style.maxHeight = `${Math.max(0, size.height - controls.offsetHeight)}px`;
2200
+ if (visual.classList.contains("ofv-tiff-pages")) {
2201
+ visual.style.removeProperty("max-height");
2202
+ } else {
2203
+ visual.style.maxHeight = `${Math.max(0, size.height - controls.offsetHeight)}px`;
2204
+ }
2205
+ updateScrollBox();
2180
2206
  },
2181
2207
  destroy() {
2182
2208
  ctx.toolbar?.setZoom(void 0);
@@ -2191,6 +2217,7 @@ function imagePlugin() {
2191
2217
  stage.removeEventListener("pointerleave", onPointerLeave);
2192
2218
  stage.removeEventListener("wheel", onWheel);
2193
2219
  image.removeEventListener("error", showImageFallback);
2220
+ image.removeEventListener("load", updateScrollBox);
2194
2221
  window.removeEventListener("blur", onWindowBlur);
2195
2222
  finishDrag();
2196
2223
  wrapper.remove();
@@ -2204,33 +2231,64 @@ function imagePlugin() {
2204
2231
  }
2205
2232
  };
2206
2233
  }
2207
- async function createTiffCanvas(bytes) {
2234
+ async function createTiffPreview(bytes, fileName) {
2208
2235
  if (bytes.byteLength === 0) {
2209
2236
  throw new Error("\u65E0\u6CD5\u8BFB\u53D6 TIFF \u6587\u4EF6\u5185\u5BB9\u3002");
2210
2237
  }
2211
2238
  const UTIF = await import("utif");
2212
2239
  const buffer = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
2213
2240
  const ifds = UTIF.decode(buffer);
2214
- const ifd = ifds.find((item) => Number(item.width || 0) > 0 || Number(item.t256 || 0) > 0) || ifds[0];
2215
- if (!ifd) {
2241
+ const imageIfds = ifds.filter((item) => hasTiffDimensions(item));
2242
+ if (imageIfds.length === 0) {
2216
2243
  throw new Error("TIFF \u6587\u4EF6\u6CA1\u6709\u53EF\u89E3\u7801\u7684\u56FE\u50CF\u76EE\u5F55\u3002");
2217
2244
  }
2245
+ const pages = imageIfds.map((ifd, index) => createTiffPage(UTIF, buffer, ifd, index, imageIfds.length, fileName));
2246
+ if (pages.length === 1) {
2247
+ return pages[0].canvas;
2248
+ }
2249
+ const container = document.createElement("div");
2250
+ container.className = "ofv-tiff-pages";
2251
+ container.setAttribute("role", "group");
2252
+ container.setAttribute("aria-label", `${fileName}\uFF0C\u5171 ${pages.length} \u9875`);
2253
+ for (const page of pages) {
2254
+ const figure = document.createElement("figure");
2255
+ figure.className = "ofv-tiff-page";
2256
+ const caption = document.createElement("figcaption");
2257
+ caption.textContent = `\u7B2C ${page.index + 1} / ${pages.length} \u9875 \xB7 ${page.width} x ${page.height}px`;
2258
+ figure.append(page.canvas, caption);
2259
+ container.append(figure);
2260
+ }
2261
+ return container;
2262
+ }
2263
+ function createTiffPage(UTIF, buffer, ifd, index, total, fileName) {
2218
2264
  UTIF.decodeImage(buffer, ifd);
2219
2265
  const rgba = UTIF.toRGBA8(ifd);
2220
- const width = Number(ifd.width || ifd.t256 || 0);
2221
- const height = Number(ifd.height || ifd.t257 || 0);
2266
+ const { width, height } = getTiffDimensions(ifd);
2222
2267
  if (!width || !height || rgba.length < width * height * 4) {
2223
2268
  throw new Error("TIFF \u56FE\u50CF\u50CF\u7D20\u6570\u636E\u4E0D\u5B8C\u6574\u3002");
2224
2269
  }
2225
2270
  const canvas = document.createElement("canvas");
2271
+ canvas.className = "ofv-tiff-canvas";
2226
2272
  canvas.width = width;
2227
2273
  canvas.height = height;
2274
+ canvas.setAttribute("role", "img");
2275
+ canvas.setAttribute("aria-label", total > 1 ? `${fileName} \u7B2C ${index + 1} \u9875` : fileName);
2228
2276
  const context = canvas.getContext("2d");
2229
2277
  if (!context) {
2230
2278
  throw new Error("\u5F53\u524D\u73AF\u5883\u4E0D\u652F\u6301 Canvas 2D\uFF0C\u65E0\u6CD5\u5C55\u793A TIFF\u3002");
2231
2279
  }
2232
2280
  context.putImageData(new ImageData(new Uint8ClampedArray(rgba), width, height), 0, 0);
2233
- return canvas;
2281
+ return { canvas, width, height, index };
2282
+ }
2283
+ function hasTiffDimensions(ifd) {
2284
+ const { width, height } = getTiffDimensions(ifd);
2285
+ return width > 0 && height > 0;
2286
+ }
2287
+ function getTiffDimensions(ifd) {
2288
+ return {
2289
+ width: Number(ifd.width || ifd.t256 || 0),
2290
+ height: Number(ifd.height || ifd.t257 || 0)
2291
+ };
2234
2292
  }
2235
2293
  function createImageFallback(fileName, url) {
2236
2294
  const fallback = document.createElement("div");
@@ -2559,6 +2617,7 @@ function parseTiffInfo(bytes) {
2559
2617
  return { format: magic === 43 ? "BigTIFF" : "TIFF", note: "IFD \u504F\u79FB\u8D85\u51FA\u6587\u4EF6\u8303\u56F4" };
2560
2618
  }
2561
2619
  const count = magic === 43 ? readTiffUint64AsNumber(bytes, ifdOffset, littleEndian) : readTiffUint16(bytes, ifdOffset, littleEndian);
2620
+ const imageCount = countTiffIfds(bytes, ifdOffset, magic === 43, littleEndian);
2562
2621
  const entrySize = magic === 43 ? 20 : 12;
2563
2622
  const entriesStart = ifdOffset + (magic === 43 ? 8 : 2);
2564
2623
  let width;
@@ -2585,9 +2644,26 @@ function parseTiffInfo(bytes) {
2585
2644
  height,
2586
2645
  bitDepth: bitDepth ? `${bitDepth} bit` : void 0,
2587
2646
  color: compression ? tiffCompressionName(compression) : void 0,
2588
- count: Number.isFinite(count) ? count : void 0
2647
+ count: imageCount || void 0
2589
2648
  };
2590
2649
  }
2650
+ function countTiffIfds(bytes, firstOffset, bigTiff, littleEndian) {
2651
+ const seen = /* @__PURE__ */ new Set();
2652
+ let offset = firstOffset;
2653
+ let count = 0;
2654
+ while (Number.isFinite(offset) && offset > 0 && offset < bytes.length && !seen.has(offset) && count < 512) {
2655
+ seen.add(offset);
2656
+ const entryCount = bigTiff ? readTiffUint64AsNumber(bytes, offset, littleEndian) : readTiffUint16(bytes, offset, littleEndian);
2657
+ const entrySize = bigTiff ? 20 : 12;
2658
+ const nextOffsetPosition = offset + (bigTiff ? 8 : 2) + entryCount * entrySize;
2659
+ if (!Number.isFinite(entryCount) || nextOffsetPosition + (bigTiff ? 8 : 4) > bytes.length) {
2660
+ break;
2661
+ }
2662
+ count++;
2663
+ offset = bigTiff ? readTiffUint64AsNumber(bytes, nextOffsetPosition, littleEndian) : readTiffUint32(bytes, nextOffsetPosition, littleEndian);
2664
+ }
2665
+ return count;
2666
+ }
2591
2667
  function readTiffInlineValue(bytes, offset, type, littleEndian) {
2592
2668
  if (offset + 4 > bytes.length) {
2593
2669
  return void 0;
@@ -3986,26 +4062,6 @@ var mimeLangMap = {
3986
4062
  };
3987
4063
  var MAX_HIGHLIGHT_CHARS = 18e4;
3988
4064
  var MAX_RENDER_CHARS = 6e5;
3989
- function loadPrismCss(theme) {
3990
- const lightId = "ofv-prism-css-light";
3991
- const darkId = "ofv-prism-css-dark";
3992
- const activeId = theme === "dark" ? darkId : lightId;
3993
- const inactiveId = theme === "dark" ? lightId : darkId;
3994
- document.getElementById(inactiveId)?.remove();
3995
- if (document.getElementById(activeId)) {
3996
- return Promise.resolve();
3997
- }
3998
- 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";
3999
- return new Promise((resolve, reject) => {
4000
- const link = document.createElement("link");
4001
- link.id = activeId;
4002
- link.rel = "stylesheet";
4003
- link.href = href;
4004
- link.onload = () => resolve();
4005
- link.onerror = () => reject(new Error(`Failed to load Prism CSS: ${href}`));
4006
- document.head.appendChild(link);
4007
- });
4008
- }
4009
4065
  function textPlugin() {
4010
4066
  return {
4011
4067
  name: "text",
@@ -4029,7 +4085,6 @@ function textPlugin() {
4029
4085
  }
4030
4086
  };
4031
4087
  }
4032
- 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";
4033
4088
  if (isMarkdown) {
4034
4089
  const [markedModule, PrismModule2, DOMPurifyModule] = await Promise.all([
4035
4090
  import("marked"),
@@ -4050,7 +4105,6 @@ function textPlugin() {
4050
4105
  try {
4051
4106
  const codeBlocks = container.querySelectorAll("pre code");
4052
4107
  if (codeBlocks.length > 0) {
4053
- await loadPrismCss(isDark ? "dark" : "light");
4054
4108
  codeBlocks.forEach((block) => {
4055
4109
  const parent = block.parentElement;
4056
4110
  if (parent && !parent.className.includes("language-")) {
@@ -4174,9 +4228,6 @@ function textPlugin() {
4174
4228
  console.warn(`Prism failed to load language component for: ${lang}`, e);
4175
4229
  }
4176
4230
  }
4177
- await loadPrismCss(isDark ? "dark" : "light").catch((error) => {
4178
- console.warn("Prism CSS failed to load; rendering code without external theme:", error);
4179
- });
4180
4231
  const codeText = text.length > MAX_RENDER_CHARS ? text.slice(0, MAX_RENDER_CHARS) : text;
4181
4232
  const totalLines = countLines(text);
4182
4233
  const shownLines = countLines(codeText);
@@ -4704,10 +4755,11 @@ async function renderPdfDocumentPreview(options) {
4704
4755
  const baseViewport = page.getViewport({ scale: 1 });
4705
4756
  pagesMeta.push({
4706
4757
  width: baseViewport.width,
4707
- height: baseViewport.height
4758
+ height: baseViewport.height,
4759
+ rotation: getPdfPageRotation(page)
4708
4760
  });
4709
4761
  } catch {
4710
- pagesMeta.push({ width: 612, height: 792 });
4762
+ pagesMeta.push({ width: 612, height: 792, rotation: 0 });
4711
4763
  }
4712
4764
  }
4713
4765
  const pageStates = [];
@@ -4742,7 +4794,7 @@ async function renderPdfDocumentPreview(options) {
4742
4794
  const page = await pdfDocument.getPage(pageIdx + 1);
4743
4795
  const meta = pagesMeta[pageIdx];
4744
4796
  const scale = options.fit === "actual" ? zoomFactor : Math.max(0.05, Math.min(5, getPdfAvailableWidth(size.width) / rotatedPdfWidth(meta, rotation) * zoomFactor));
4745
- const viewport = page.getViewport({ scale, rotation });
4797
+ const viewport = page.getViewport({ scale, rotation: getPdfRenderRotation(meta, rotation) });
4746
4798
  const outputScale = getPdfOutputScale();
4747
4799
  const cssWidth = Math.floor(viewport.width);
4748
4800
  const cssHeight = Math.floor(viewport.height);
@@ -4781,6 +4833,8 @@ async function renderPdfDocumentPreview(options) {
4781
4833
  const span = document.createElement("span");
4782
4834
  span.textContent = str;
4783
4835
  span.style.fontSize = `${fontHeight}px`;
4836
+ span.style.lineHeight = "1";
4837
+ span.style.height = `${fontHeight}px`;
4784
4838
  span.style.fontFamily = item.fontName || "sans-serif";
4785
4839
  span.style.left = `${tx[4]}px`;
4786
4840
  span.style.top = `${tx[5] - fontHeight}px`;
@@ -4978,6 +5032,13 @@ function appendPdfSummary(parent, label, value) {
4978
5032
  function normalizePdfRotation(value) {
4979
5033
  return (value % 360 + 360) % 360;
4980
5034
  }
5035
+ function getPdfPageRotation(page) {
5036
+ const rotation = Number(page.rotate);
5037
+ return Number.isFinite(rotation) ? normalizePdfRotation(rotation) : 0;
5038
+ }
5039
+ function getPdfRenderRotation(meta, userRotation) {
5040
+ return normalizePdfRotation((meta.rotation || 0) + userRotation);
5041
+ }
4981
5042
  function isPdfRotatedSideways(rotation) {
4982
5043
  const normalized = normalizePdfRotation(rotation);
4983
5044
  return normalized === 90 || normalized === 270;
@@ -5948,13 +6009,13 @@ function officePlugin(options = {}) {
5948
6009
  if (conversionContext && await shouldUseOfficeConversion(options, conversionContext)) {
5949
6010
  delegatedInstance = await renderConvertedOfficePreview(panel, ctx, options, conversionContext);
5950
6011
  } else if (packageFormat === "docx" && !fileIsDocx(extension)) {
5951
- disposeDocxFit = await renderDocx(panel, arrayBuffer);
6012
+ disposeDocxFit = await renderDocx(panel, arrayBuffer, ctx.options.fit);
5952
6013
  } else if (packageFormat === "xlsx" && !sheetExtensions.has(extension)) {
5953
6014
  await renderSheet(panel, arrayBuffer, "xlsx");
5954
6015
  } else if (packageFormat === "pptx" && !["pptx", "pptm", "ppsx", "ppsm", "potx", "potm"].includes(extension)) {
5955
6016
  await renderPptx(panel, arrayBuffer);
5956
6017
  } else if (fileIsDocx(extension)) {
5957
- disposeDocxFit = await renderDocx(panel, arrayBuffer);
6018
+ disposeDocxFit = await renderDocx(panel, arrayBuffer, ctx.options.fit);
5958
6019
  } else if (extension === "rtf") {
5959
6020
  renderPlainDocument(panel, "RTF \u6587\u6863", rtfToText(await readTextFromBuffer(arrayBuffer)));
5960
6021
  } else if (extension === "odt") {
@@ -5963,7 +6024,7 @@ function officePlugin(options = {}) {
5963
6024
  renderOpenDocumentXml(panel, "FODT \u6587\u6863", await readTextFromBuffer(arrayBuffer));
5964
6025
  } else if (extension === "fods") {
5965
6026
  renderFlatOds(panel, await readTextFromBuffer(arrayBuffer));
5966
- } else if (packagedOfficeCandidates.has(extension) && await renderPackagedOfficePreview(panel, arrayBuffer, extension)) {
6027
+ } else if (packagedOfficeCandidates.has(extension) && await renderPackagedOfficePreview(panel, arrayBuffer, extension, ctx.options.fit)) {
5967
6028
  } else if (sheetExtensions.has(extension)) {
5968
6029
  await renderSheet(panel, arrayBuffer, extension);
5969
6030
  } else if (["pptx", "pptm", "ppsx", "ppsm", "potx", "potm"].includes(extension)) {
@@ -6165,7 +6226,7 @@ async function detectPackagedOfficeFormat(arrayBuffer) {
6165
6226
  }
6166
6227
  return void 0;
6167
6228
  }
6168
- async function renderDocx(panel, arrayBuffer) {
6229
+ async function renderDocx(panel, arrayBuffer, fit) {
6169
6230
  const content = document.createElement("div");
6170
6231
  content.className = "ofv-docx-document";
6171
6232
  const styleContainer = document.createElement("div");
@@ -6204,7 +6265,7 @@ async function renderDocx(panel, arrayBuffer) {
6204
6265
  return () => void 0;
6205
6266
  }
6206
6267
  panel.append(content);
6207
- disposeFit = fitDocxPages(content);
6268
+ disposeFit = fitDocxPages(content, fit);
6208
6269
  return () => {
6209
6270
  disposeFit?.();
6210
6271
  styleContainer.remove();
@@ -6802,7 +6863,8 @@ function looksLikeDocxTextboxHeading(value) {
6802
6863
  return text.length > 0 && text.length <= 12 && !/[0-9@.::]/.test(text);
6803
6864
  }
6804
6865
  async function normalizeDocxLayout(container, arrayBuffer) {
6805
- const hints = await readDocxLayoutHints(arrayBuffer);
6866
+ const [hints, charts] = await Promise.all([readDocxLayoutHints(arrayBuffer), readDocxCharts(arrayBuffer)]);
6867
+ repairDocxChartPlaceholders(container, charts);
6806
6868
  const pages = container.querySelectorAll("section.ofv-docx");
6807
6869
  for (const page of pages) {
6808
6870
  repairDocxShapeFills(page);
@@ -6817,6 +6879,75 @@ async function normalizeDocxLayout(container, arrayBuffer) {
6817
6879
  }
6818
6880
  }
6819
6881
  }
6882
+ async function readDocxCharts(arrayBuffer) {
6883
+ try {
6884
+ const zip = await import_jszip3.default.loadAsync(arrayBuffer);
6885
+ const documentXml = await zip.file("word/document.xml")?.async("text");
6886
+ const documentDoc = documentXml ? parseOfficeXml(documentXml) : void 0;
6887
+ if (!documentDoc) {
6888
+ return [];
6889
+ }
6890
+ const relationships = await readOfficeRelationships(zip, "word/document.xml");
6891
+ const chartDrawings = Array.from(documentDoc.getElementsByTagName("*")).filter((element) => element.localName === "inline" || element.localName === "anchor").map((element) => readDocxChartDrawing(element)).filter((item) => Boolean(item?.relationshipId));
6892
+ const charts = [];
6893
+ for (const [index, drawing] of chartDrawings.entries()) {
6894
+ const chartRel = relationships.find((rel) => rel.id === drawing.relationshipId && /\/chart$/i.test(rel.type));
6895
+ const chartPath = resolveOfficeRelationshipTarget("word/document.xml", chartRel?.target);
6896
+ const chartXml = chartPath ? await zip.file(chartPath)?.async("text") : void 0;
6897
+ const chart = chartXml ? parseChartXml(chartXml, chartPath?.split("/").pop() || `chart${index + 1}.xml`) : null;
6898
+ if (chart) {
6899
+ charts.push({ ...chart, widthPt: drawing.widthPt, heightPt: drawing.heightPt });
6900
+ }
6901
+ }
6902
+ return charts;
6903
+ } catch {
6904
+ return [];
6905
+ }
6906
+ }
6907
+ function readDocxChartDrawing(element) {
6908
+ const chart = Array.from(element.getElementsByTagName("*")).find((child) => child.localName === "chart");
6909
+ const relationshipId = chart ? getXmlAttribute3(chart, "id") : "";
6910
+ if (!relationshipId) {
6911
+ return void 0;
6912
+ }
6913
+ const extent = Array.from(element.children).find((child) => child.localName === "extent");
6914
+ return {
6915
+ relationshipId,
6916
+ widthPt: emuToPt(Number(extent?.getAttribute("cx") || 0)),
6917
+ heightPt: emuToPt(Number(extent?.getAttribute("cy") || 0))
6918
+ };
6919
+ }
6920
+ function repairDocxChartPlaceholders(container, charts) {
6921
+ if (charts.length === 0) {
6922
+ return;
6923
+ }
6924
+ const placeholders = Array.from(container.querySelectorAll("section.ofv-docx div")).filter(isDocxChartPlaceholder);
6925
+ if (placeholders.length !== charts.length) {
6926
+ return;
6927
+ }
6928
+ placeholders.forEach((placeholder, index) => {
6929
+ const chart = charts[index];
6930
+ placeholder.classList.add("ofv-docx-chart-preview");
6931
+ placeholder.dataset.ofvDocxChartPreview = "true";
6932
+ if (chart.widthPt > 0) {
6933
+ placeholder.style.width ||= `${formatCssNumber(chart.widthPt)}pt`;
6934
+ }
6935
+ if (chart.heightPt > 0) {
6936
+ placeholder.style.height ||= `${formatCssNumber(chart.heightPt)}pt`;
6937
+ }
6938
+ placeholder.replaceChildren(renderChartSvg(chart));
6939
+ });
6940
+ }
6941
+ function isDocxChartPlaceholder(element) {
6942
+ if (element.dataset.ofvDocxChartPreview === "true" || element.children.length > 0 || normalizePreviewText(element.textContent || "")) {
6943
+ return false;
6944
+ }
6945
+ const display = element.style.display;
6946
+ const position = element.style.position;
6947
+ const width = parseCssPixelValue(element.style.width);
6948
+ const height = parseCssPixelValue(element.style.height);
6949
+ return display === "inline-block" && position === "relative" && width >= 120 && height >= 80;
6950
+ }
6820
6951
  function repairDocxHeadingShapeAlignment(page) {
6821
6952
  for (const paragraph of page.querySelectorAll("p")) {
6822
6953
  const text = normalizePreviewText(paragraph.textContent || "");
@@ -6967,14 +7098,19 @@ function repairDocxShapeFills(page) {
6967
7098
  }
6968
7099
  }
6969
7100
  function repairDocxFloatingPictures(page, hints) {
6970
- const hint = hints.floatingPictures.find((item) => item.relativeFrom === "column" && item.wrap === "square");
6971
- if (!hint) {
7101
+ const pageHints = hints.floatingPictures.filter((item) => item.relativeFrom === "column" && item.wrap === "square");
7102
+ if (pageHints.length === 0) {
6972
7103
  return;
6973
7104
  }
6974
- const image = page.querySelector("img");
6975
- if (!image) {
7105
+ const images = Array.from(page.querySelectorAll("img")).filter(
7106
+ (image) => image.closest("[data-ofv-docx-float-repaired='true']") === null
7107
+ );
7108
+ if (images.length === 0 || images.length !== pageHints.length) {
6976
7109
  return;
6977
7110
  }
7111
+ images.forEach((image, index) => repairDocxFloatingPicture(page, image, pageHints[index]));
7112
+ }
7113
+ function repairDocxFloatingPicture(page, image, hint) {
6978
7114
  const wrapper = image.parentElement;
6979
7115
  if (!wrapper || wrapper.dataset.ofvDocxFloatRepaired === "true") {
6980
7116
  return;
@@ -7020,7 +7156,7 @@ function parseCssLineHeight(value) {
7020
7156
  const parsed = Number.parseFloat(trimmed);
7021
7157
  return Number.isFinite(parsed) ? parsed : 0;
7022
7158
  }
7023
- function fitDocxPages(container) {
7159
+ function fitDocxPages(container, fit) {
7024
7160
  const wrapper = container.querySelector(".ofv-docx-wrapper");
7025
7161
  if (!wrapper) {
7026
7162
  return () => void 0;
@@ -7033,6 +7169,7 @@ function fitDocxPages(container) {
7033
7169
  return;
7034
7170
  }
7035
7171
  const availableWidth = Math.max(1, container.clientWidth - 48);
7172
+ const availableHeight = container.clientHeight > 0 ? Math.max(1, container.clientHeight - 48) : void 0;
7036
7173
  const pageWidth = Math.max(
7037
7174
  1,
7038
7175
  ...frames.map(({ page }) => {
@@ -7040,14 +7177,24 @@ function fitDocxPages(container) {
7040
7177
  return page.offsetWidth || rectWidth || parseCssPixelValue(page.style.width) || 794;
7041
7178
  })
7042
7179
  );
7043
- const scale = Math.min(1, Math.max(0.35, availableWidth / pageWidth));
7180
+ const pageHeight = Math.max(
7181
+ 1,
7182
+ ...frames.map(({ page }) => page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height) || 1123)
7183
+ );
7184
+ const scale = getDocxFitScale(fit, availableWidth, availableHeight, pageWidth, pageHeight);
7044
7185
  const userZoom = parseCssPixelValue(panel?.style.getPropertyValue("--ofv-office-zoom") || "1") || 1;
7045
7186
  wrapper.style.setProperty("--ofv-docx-scale", formatCssNumber(scale));
7046
7187
  wrapper.style.setProperty("--ofv-docx-page-width", `${pageWidth}px`);
7188
+ wrapper.style.width = `${Math.ceil(pageWidth * scale * userZoom + 48)}px`;
7189
+ wrapper.style.maxWidth = "none";
7190
+ wrapper.style.overflow = "visible";
7047
7191
  for (const { frame, page } of frames) {
7048
- const pageHeight = page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height);
7049
- if (pageHeight > 0) {
7050
- frame.style.height = `${Math.ceil(pageHeight * scale * userZoom)}px`;
7192
+ const framePageHeight = page.offsetHeight || page.getBoundingClientRect().height || parseCssPixelValue(page.style.height);
7193
+ const framePageWidth = page.offsetWidth || page.getBoundingClientRect().width || parseCssPixelValue(page.style.width) || pageWidth;
7194
+ frame.style.width = `${Math.ceil(framePageWidth * scale * userZoom)}px`;
7195
+ frame.style.maxWidth = "none";
7196
+ if (framePageHeight > 0) {
7197
+ frame.style.height = `${Math.ceil(framePageHeight * scale * userZoom)}px`;
7051
7198
  }
7052
7199
  }
7053
7200
  };
@@ -7072,6 +7219,26 @@ function fitDocxPages(container) {
7072
7219
  observer.disconnect();
7073
7220
  };
7074
7221
  }
7222
+ function getDocxFitScale(fit, availableWidth, availableHeight, pageWidth, pageHeight) {
7223
+ const widthScale = availableWidth / pageWidth;
7224
+ const heightScale = availableHeight ? availableHeight / pageHeight : void 0;
7225
+ if (fit === "actual") {
7226
+ return 1;
7227
+ }
7228
+ if (fit === "height") {
7229
+ return Math.max(0.1, heightScale ?? widthScale);
7230
+ }
7231
+ if (fit === "cover") {
7232
+ return Math.max(0.1, Math.max(widthScale, heightScale ?? widthScale));
7233
+ }
7234
+ if (fit === "scale-down") {
7235
+ return Math.min(1, Math.max(0.1, Math.min(widthScale, heightScale ?? widthScale)));
7236
+ }
7237
+ if (fit === "contain") {
7238
+ return Math.max(0.1, Math.min(widthScale, heightScale ?? widthScale));
7239
+ }
7240
+ return Math.max(0.1, widthScale);
7241
+ }
7075
7242
  function ensureDocxPageFrames(wrapper) {
7076
7243
  const pages = Array.from(wrapper.querySelectorAll("section.ofv-docx"));
7077
7244
  return pages.map((page) => {
@@ -7603,9 +7770,9 @@ function parseChartXml(xml, fallbackName) {
7603
7770
  return null;
7604
7771
  }
7605
7772
  const type = detectChartType(doc);
7606
- const title = textFromFirst(doc, "title") || fallbackName.replace(/\.xml$/i, "");
7773
+ const title = readChartTitle(doc, fallbackName);
7607
7774
  const seriesElements = Array.from(doc.getElementsByTagName("*")).filter((element) => element.localName === "ser");
7608
- const series = seriesElements.map((element, index) => parseChartSeries(element, index)).filter((item) => item.values.length > 0);
7775
+ const series = seriesElements.map((element, index) => ({ ...parseChartSeries(element, index), color: readChartSeriesColor(element) })).filter((item) => item.values.length > 0);
7609
7776
  if (series.length === 0) {
7610
7777
  return null;
7611
7778
  }
@@ -7614,9 +7781,21 @@ function parseChartXml(xml, fallbackName) {
7614
7781
  type,
7615
7782
  title,
7616
7783
  categories: series.find((item) => item.categories.length > 0)?.categories || [],
7617
- series: series.map((item) => ({ name: item.name, values: item.values }))
7784
+ series: series.map((item) => ({ name: item.name, values: item.values, color: item.color }))
7618
7785
  };
7619
7786
  }
7787
+ function readChartTitle(doc, fallbackName) {
7788
+ const titleElement = Array.from(doc.getElementsByTagName("*")).find((element) => element.localName === "title");
7789
+ if (!titleElement) {
7790
+ return fallbackName.replace(/\.xml$/i, "");
7791
+ }
7792
+ const explicitTitle = chartText(titleElement);
7793
+ if (explicitTitle) {
7794
+ return explicitTitle;
7795
+ }
7796
+ const language = Array.from(doc.getElementsByTagName("*")).find((element) => element.localName === "lang")?.getAttribute("val") || "";
7797
+ return /^zh\b/i.test(language) ? "\u56FE\u8868\u6807\u9898" : "Chart Title";
7798
+ }
7620
7799
  function detectChartType(doc) {
7621
7800
  const chartType = Array.from(doc.getElementsByTagName("*")).find(
7622
7801
  (element) => element.localName.endsWith("Chart") && element.localName !== "chart"
@@ -7633,6 +7812,31 @@ function parseChartSeries(element, index) {
7633
7812
  categories: stringsFromFirst(element, "cat")
7634
7813
  };
7635
7814
  }
7815
+ function readChartSeriesColor(element) {
7816
+ const shape = Array.from(element?.children || []).find((child) => child.localName === "spPr");
7817
+ const color = Array.from(shape?.getElementsByTagName("*") || []).find(
7818
+ (child) => child.localName === "srgbClr" || child.localName === "schemeClr"
7819
+ );
7820
+ if (!color) {
7821
+ return void 0;
7822
+ }
7823
+ if (color.localName === "srgbClr") {
7824
+ const value = color.getAttribute("val") || "";
7825
+ return /^[\da-f]{6}$/i.test(value) ? `#${value}` : void 0;
7826
+ }
7827
+ return chartSchemeColor(color.getAttribute("val") || "");
7828
+ }
7829
+ function chartSchemeColor(value) {
7830
+ const colors = {
7831
+ accent1: "#156082",
7832
+ accent2: "#e97132",
7833
+ accent3: "#196b24",
7834
+ accent4: "#0f9ed5",
7835
+ accent5: "#a02b93",
7836
+ accent6: "#4ea72e"
7837
+ };
7838
+ return colors[value];
7839
+ }
7636
7840
  function renderChartPreviewSection(charts) {
7637
7841
  const section = createSection("\u8868\u683C\u56FE\u8868\u9884\u89C8");
7638
7842
  const grid = document.createElement("div");
@@ -7670,17 +7874,39 @@ function renderChartCard(chart) {
7670
7874
  }
7671
7875
  function renderChartSvg(chart) {
7672
7876
  const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
7673
- svg.setAttribute("viewBox", "0 0 640 260");
7877
+ svg.setAttribute("viewBox", "0 0 640 380");
7674
7878
  svg.setAttribute("role", "img");
7675
7879
  svg.setAttribute("aria-label", chart.title);
7676
7880
  svg.classList.add("ofv-chart-svg");
7677
7881
  const allValues = chart.series.flatMap((item) => item.values).filter((value) => Number.isFinite(value));
7678
7882
  const max = Math.max(1, ...allValues);
7679
7883
  const min = Math.min(0, ...allValues);
7884
+ const axisScale = createChartAxisScale(max, min);
7885
+ const axisMax = axisScale.max;
7886
+ const axisMin = axisScale.min;
7680
7887
  const span = max - min || 1;
7681
- const colors = ["#2563eb", "#059669", "#d97706", "#dc2626", "#7c3aed"];
7682
- const plot = { x: 48, y: 24, width: 552, height: 178 };
7683
- appendSvg(svg, "line", { x1: plot.x, y1: plot.y, x2: plot.x, y2: plot.y + plot.height, class: "ofv-chart-axis" });
7888
+ const colors = ["#156082", "#e97132", "#196b24", "#0f9ed5", "#a02b93", "#4ea72e"];
7889
+ const plot = { x: 74, y: 74, width: 526, height: 214 };
7890
+ const categories = chart.categories.length > 0 ? chart.categories : chart.series[0]?.values.map((_, index) => String(index + 1)) || [];
7891
+ const title = appendSvg(svg, "text", { x: 320, y: 34, class: "ofv-chart-title", "text-anchor": "middle" });
7892
+ title.textContent = chart.title;
7893
+ for (const value of axisScale.ticks) {
7894
+ const y = plot.y + plot.height - (value - axisMin) / (axisMax - axisMin || 1) * plot.height;
7895
+ appendSvg(svg, "line", {
7896
+ x1: plot.x,
7897
+ y1: Number(y.toFixed(1)),
7898
+ x2: plot.x + plot.width,
7899
+ y2: Number(y.toFixed(1)),
7900
+ class: value === 0 ? "ofv-chart-axis" : "ofv-chart-gridline"
7901
+ });
7902
+ const label = appendSvg(svg, "text", {
7903
+ x: plot.x - 12,
7904
+ y: Number((y + 4).toFixed(1)),
7905
+ class: "ofv-chart-label",
7906
+ "text-anchor": "end"
7907
+ });
7908
+ label.textContent = formatChartTick(value);
7909
+ }
7684
7910
  appendSvg(svg, "line", {
7685
7911
  x1: plot.x,
7686
7912
  y1: plot.y + plot.height,
@@ -7688,28 +7914,47 @@ function renderChartSvg(chart) {
7688
7914
  y2: plot.y + plot.height,
7689
7915
  class: "ofv-chart-axis"
7690
7916
  });
7691
- chart.series.forEach((series, seriesIndex) => {
7692
- const color = colors[seriesIndex % colors.length];
7693
- const step = series.values.length > 1 ? plot.width / (series.values.length - 1) : plot.width;
7694
- const points = series.values.map((value, index) => ({
7695
- x: plot.x + index * step,
7696
- y: plot.y + plot.height - (value - min) / span * plot.height
7697
- }));
7698
- if (chart.type.includes("bar") || chart.type.includes("col")) {
7699
- const barWidth = Math.max(4, Math.min(28, step * 0.6)) / Math.max(1, chart.series.length);
7700
- points.forEach((point, index) => {
7701
- const zeroY = plot.y + plot.height - (0 - min) / span * plot.height;
7702
- const x = point.x - barWidth * chart.series.length / 2 + seriesIndex * barWidth;
7917
+ if (chart.type.includes("bar") || chart.type.includes("col")) {
7918
+ const categoryCount = Math.max(1, categories.length, ...chart.series.map((series) => series.values.length));
7919
+ const groupWidth = plot.width / categoryCount;
7920
+ const clusterWidth = groupWidth * 0.58;
7921
+ const barWidth = Math.max(5, Math.min(28, clusterWidth / Math.max(1, chart.series.length)));
7922
+ const zeroY = plot.y + plot.height - (0 - axisMin) / (axisMax - axisMin || 1) * plot.height;
7923
+ categories.slice(0, categoryCount).forEach((category, index) => {
7924
+ const x = plot.x + groupWidth * (index + 0.5);
7925
+ const label = appendSvg(svg, "text", {
7926
+ x: Number(x.toFixed(1)),
7927
+ y: plot.y + plot.height + 22,
7928
+ class: "ofv-chart-label",
7929
+ "text-anchor": "middle"
7930
+ });
7931
+ label.textContent = truncateChartLabel(category);
7932
+ });
7933
+ chart.series.forEach((series, seriesIndex) => {
7934
+ const color = series.color || colors[seriesIndex % colors.length];
7935
+ series.values.forEach((value, index) => {
7936
+ const groupCenter = plot.x + groupWidth * (index + 0.5);
7937
+ const x = groupCenter - barWidth * chart.series.length / 2 + seriesIndex * barWidth + barWidth * 0.12;
7938
+ const y = plot.y + plot.height - (value - axisMin) / (axisMax - axisMin || 1) * plot.height;
7703
7939
  appendSvg(svg, "rect", {
7704
- x,
7705
- y: Math.min(point.y, zeroY),
7706
- width: barWidth,
7707
- height: Math.max(1, Math.abs(zeroY - point.y)),
7940
+ x: Number(x.toFixed(1)),
7941
+ y: Number(Math.min(y, zeroY).toFixed(1)),
7942
+ width: Number((barWidth * 0.76).toFixed(1)),
7943
+ height: Number(Math.max(1, Math.abs(zeroY - y)).toFixed(1)),
7708
7944
  fill: color,
7709
7945
  "data-index": index
7710
7946
  });
7711
7947
  });
7712
- } else {
7948
+ });
7949
+ } else {
7950
+ chart.series.forEach((series, seriesIndex) => {
7951
+ const color = series.color || colors[seriesIndex % colors.length];
7952
+ const step = series.values.length > 1 ? plot.width / (series.values.length - 1) : plot.width;
7953
+ const lineSpan = axisMax - axisMin || span;
7954
+ const points = series.values.map((value, index) => ({
7955
+ x: plot.x + index * step,
7956
+ y: plot.y + plot.height - (value - axisMin) / lineSpan * plot.height
7957
+ }));
7713
7958
  appendSvg(svg, "polyline", {
7714
7959
  points: points.map((point) => `${point.x.toFixed(1)},${point.y.toFixed(1)}`).join(" "),
7715
7960
  fill: "none",
@@ -7721,11 +7966,49 @@ function renderChartSvg(chart) {
7721
7966
  for (const point of points.slice(0, 80)) {
7722
7967
  appendSvg(svg, "circle", { cx: point.x, cy: point.y, r: 3, fill: color });
7723
7968
  }
7724
- }
7725
- appendLegend(svg, series.name, color, 52 + seriesIndex * 118, 236);
7726
- });
7969
+ });
7970
+ }
7971
+ appendChartLegend(svg, chart, colors, 348);
7727
7972
  return svg;
7728
7973
  }
7974
+ function appendChartLegend(svg, chart, colors, y) {
7975
+ const itemWidth = 86;
7976
+ const startX = 320 - chart.series.length * itemWidth / 2;
7977
+ chart.series.forEach((series, seriesIndex) => {
7978
+ const color = series.color || colors[seriesIndex % colors.length];
7979
+ appendLegend(svg, series.name, color, startX + seriesIndex * itemWidth, y);
7980
+ });
7981
+ }
7982
+ function createChartAxisScale(max, min) {
7983
+ const axisMin = Math.min(0, min);
7984
+ const positiveMax = Math.max(1, max);
7985
+ const step = niceChartStep((positiveMax - axisMin) / 5);
7986
+ let axisMax = Math.ceil(positiveMax / step) * step;
7987
+ if (axisMax <= positiveMax) {
7988
+ axisMax += step;
7989
+ }
7990
+ const ticks = [];
7991
+ for (let value = axisMin; value <= axisMax + step / 2; value += step) {
7992
+ ticks.push(Number(value.toFixed(6)));
7993
+ }
7994
+ return { min: axisMin, max: axisMax, ticks };
7995
+ }
7996
+ function niceChartStep(rawStep) {
7997
+ if (rawStep <= 0) {
7998
+ return 1;
7999
+ }
8000
+ const magnitude = 10 ** Math.floor(Math.log10(rawStep));
8001
+ const normalized = rawStep / magnitude;
8002
+ const nice = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10;
8003
+ return nice * magnitude;
8004
+ }
8005
+ function formatChartTick(value) {
8006
+ const rounded = Math.abs(value) < 1 ? Number(value.toFixed(1)) : Number(value.toFixed(0));
8007
+ return String(rounded);
8008
+ }
8009
+ function truncateChartLabel(value) {
8010
+ return value.length > 10 ? `${value.slice(0, 10)}...` : value;
8011
+ }
7729
8012
  function appendLegend(svg, label, color, x, y) {
7730
8013
  appendSvg(svg, "rect", { x, y: y - 10, width: 12, height: 12, rx: 2, fill: color });
7731
8014
  const text = appendSvg(svg, "text", { x: x + 18, y, class: "ofv-chart-label" });
@@ -8459,7 +8742,7 @@ function renderOpenDocumentPresentationXml(panel, xml) {
8459
8742
  renderPresentationInsight(panel, inspectOpenDocumentPresentation("FODP \u6F14\u793A\u6587\u7A3F", xml, 0));
8460
8743
  renderOpenDocumentPresentation(panel, "FODP \u6F14\u793A\u6587\u7A3F", xml, []);
8461
8744
  }
8462
- async function renderPackagedOfficePreview(panel, arrayBuffer, extension) {
8745
+ async function renderPackagedOfficePreview(panel, arrayBuffer, extension, fit) {
8463
8746
  let zip;
8464
8747
  try {
8465
8748
  zip = await import_jszip3.default.loadAsync(arrayBuffer);
@@ -8470,7 +8753,7 @@ async function renderPackagedOfficePreview(panel, arrayBuffer, extension) {
8470
8753
  const hasEntry = (path) => entries.some((entry) => entry.name.toLowerCase() === path.toLowerCase());
8471
8754
  const contentXml = zip.file(/(^|\/)content\.xml$/i)[0];
8472
8755
  if (hasEntry("word/document.xml")) {
8473
- await renderDocx(panel, arrayBuffer);
8756
+ await renderDocx(panel, arrayBuffer, fit);
8474
8757
  return true;
8475
8758
  }
8476
8759
  if (hasEntry("xl/workbook.xml")) {