@open-file-viewer/core 0.1.11 → 0.1.12

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/README.md CHANGED
@@ -123,6 +123,44 @@ pdfPlugin({
123
123
  This keeps compatibility at the cost of holding one extra copy of the PDF in memory, so use it only
124
124
  for affected environments.
125
125
 
126
+ ## High-Fidelity Office Conversion
127
+
128
+ Browser-side Office renderers cannot perfectly reproduce Word/WPS layout for files with anchored
129
+ textboxes, absolute positioning, custom fonts, headers/footers or legacy binary formats. For those
130
+ files, configure `officePlugin({ convert })` to send the file to your own LibreOffice, OnlyOffice or
131
+ Microsoft Graph conversion service and return a PDF. The converted PDF is rendered by the built-in
132
+ PDF viewer.
133
+
134
+ ```ts
135
+ officePlugin({
136
+ pdf: { workerSrc: pdfWorkerSrc },
137
+ async convert({ file, arrayBuffer, reason }) {
138
+ const form = new FormData();
139
+ form.append("file", new Blob([arrayBuffer]), file.name);
140
+ form.append("reason", reason);
141
+
142
+ const response = await fetch("/api/office/convert-to-pdf", {
143
+ method: "POST",
144
+ body: form
145
+ });
146
+
147
+ if (!response.ok) {
148
+ throw new Error("Office conversion failed");
149
+ }
150
+
151
+ return {
152
+ blob: await response.blob(),
153
+ fileName: file.name.replace(/\.[^.]+$/, ".pdf"),
154
+ mimeType: "application/pdf"
155
+ };
156
+ }
157
+ });
158
+ ```
159
+
160
+ Open File Viewer does not upload files by default. The conversion hook is only called when you
161
+ explicitly configure it, and currently targets `complex-docx` and `legacy-office` cases. You can also
162
+ return `{ url, fileName, mimeType: "application/pdf" }` when your service stores the converted PDF.
163
+
126
164
  ## CAD Customization
127
165
 
128
166
  `cadPlugin()` has two CAD preview layers:
package/dist/index.cjs CHANGED
@@ -5855,7 +5855,7 @@ var officeMimeFormatMap = {
5855
5855
  "application/vnd.oasis.opendocument.presentation-flat-xml": "fodp",
5856
5856
  "application/vnd.apple.keynote": "key"
5857
5857
  };
5858
- function officePlugin() {
5858
+ function officePlugin(options = {}) {
5859
5859
  return {
5860
5860
  name: "office",
5861
5861
  match(file) {
@@ -5868,7 +5868,11 @@ function officePlugin() {
5868
5868
  const arrayBuffer = await readArrayBuffer(ctx.file);
5869
5869
  const packageFormat = shouldSniffPackagedOffice(extension) ? await detectPackagedOfficeFormat(arrayBuffer) : void 0;
5870
5870
  let disposeDocxFit;
5871
- if (packageFormat === "docx" && !fileIsDocx(extension)) {
5871
+ let delegatedInstance;
5872
+ const conversionContext = await createOfficeConversionContext(ctx, arrayBuffer, extension, packageFormat);
5873
+ if (conversionContext && await shouldUseOfficeConversion(options, conversionContext)) {
5874
+ delegatedInstance = await renderConvertedOfficePreview(panel, ctx, options, conversionContext);
5875
+ } else if (packageFormat === "docx" && !fileIsDocx(extension)) {
5872
5876
  disposeDocxFit = await renderDocx(panel, arrayBuffer);
5873
5877
  } else if (packageFormat === "xlsx" && !sheetExtensions.has(extension)) {
5874
5878
  await renderSheet(panel, arrayBuffer, "xlsx");
@@ -5902,12 +5906,13 @@ function officePlugin() {
5902
5906
  ctx.toolbar?.refreshCommandSupport();
5903
5907
  return {
5904
5908
  canCommand(command) {
5905
- return controller?.canCommand(command) ?? false;
5909
+ return delegatedInstance?.canCommand?.(command) || controller?.canCommand(command) || false;
5906
5910
  },
5907
5911
  command(command) {
5908
- return controller?.command(command) ?? false;
5912
+ return delegatedInstance?.command?.(command) || controller?.command(command) || false;
5909
5913
  },
5910
5914
  destroy() {
5915
+ delegatedInstance?.destroy();
5911
5916
  controller?.destroy();
5912
5917
  disposeDocxFit?.();
5913
5918
  panel.remove();
@@ -5916,6 +5921,104 @@ function officePlugin() {
5916
5921
  }
5917
5922
  };
5918
5923
  }
5924
+ async function createOfficeConversionContext(ctx, arrayBuffer, extension, detectedFormat) {
5925
+ const effectiveFormat = detectedFormat || extension;
5926
+ if ((effectiveFormat === "docx" || fileIsDocx(extension)) && await docxShouldPreferTextboxLayoutFallback(arrayBuffer)) {
5927
+ return { file: ctx.file, arrayBuffer, extension, detectedFormat, reason: "complex-docx" };
5928
+ }
5929
+ if (isLegacyOfficeBinary(extension)) {
5930
+ return { file: ctx.file, arrayBuffer, extension, detectedFormat, reason: "legacy-office" };
5931
+ }
5932
+ return void 0;
5933
+ }
5934
+ async function shouldUseOfficeConversion(options, context) {
5935
+ if (!options.convert) {
5936
+ return false;
5937
+ }
5938
+ if (typeof options.preferConversion === "function") {
5939
+ return Boolean(await options.preferConversion(context));
5940
+ }
5941
+ if (options.preferConversion !== void 0) {
5942
+ return options.preferConversion;
5943
+ }
5944
+ return context.reason === "complex-docx" || context.reason === "legacy-office";
5945
+ }
5946
+ async function renderConvertedOfficePreview(panel, ctx, options, conversionContext) {
5947
+ if (!options.convert) {
5948
+ throw new Error("Office conversion handler is not configured.");
5949
+ }
5950
+ const converted = normalizeOfficeConversionResult(await options.convert(conversionContext), ctx.file.name);
5951
+ if (!converted) {
5952
+ throw new Error("Office conversion handler did not return a previewable file.");
5953
+ }
5954
+ if (converted.mimeType !== "application/pdf" && !converted.fileName.toLowerCase().endsWith(".pdf")) {
5955
+ throw new Error("Office conversion handler must return a PDF Blob, ArrayBuffer or URL.");
5956
+ }
5957
+ return renderPdfDocumentPreview({
5958
+ ...options.pdf || {},
5959
+ fileName: converted.fileName,
5960
+ fileUrl: converted.fileUrl,
5961
+ fileSize: converted.fileSize,
5962
+ isExternal: !converted.revokeUrlOnDestroy,
5963
+ viewport: panel,
5964
+ size: ctx.size,
5965
+ fit: ctx.options.fit,
5966
+ toolbar: ctx.toolbar,
5967
+ title: "Office \u9AD8\u4FDD\u771F\u8F6C\u6362\u9884\u89C8",
5968
+ fallbackTitle: "Office \u8F6C\u6362\u540E\u7684 PDF \u65E0\u6CD5\u9884\u89C8",
5969
+ revokeUrlOnDestroy: converted.revokeUrlOnDestroy
5970
+ });
5971
+ }
5972
+ function normalizeOfficeConversionResult(result, sourceFileName) {
5973
+ if (!result) {
5974
+ return void 0;
5975
+ }
5976
+ const fallbackFileName = `${stripFileExtension(sourceFileName) || "office-preview"}.pdf`;
5977
+ if (typeof result === "string") {
5978
+ return {
5979
+ fileName: fallbackFileName,
5980
+ fileUrl: result,
5981
+ mimeType: "application/pdf",
5982
+ revokeUrlOnDestroy: false
5983
+ };
5984
+ }
5985
+ if (result instanceof ArrayBuffer) {
5986
+ const blob = new Blob([result], { type: "application/pdf" });
5987
+ return createConvertedOfficeBlobPreview(blob, fallbackFileName);
5988
+ }
5989
+ if (result instanceof Blob) {
5990
+ return createConvertedOfficeBlobPreview(result, fallbackFileName);
5991
+ }
5992
+ if (result.url) {
5993
+ return {
5994
+ fileName: result.fileName || fallbackFileName,
5995
+ fileUrl: result.url,
5996
+ mimeType: result.mimeType || "application/pdf",
5997
+ revokeUrlOnDestroy: false
5998
+ };
5999
+ }
6000
+ const data = result.blob || result.data;
6001
+ if (data instanceof ArrayBuffer) {
6002
+ const blob = new Blob([data], { type: result.mimeType || "application/pdf" });
6003
+ return createConvertedOfficeBlobPreview(blob, result.fileName || fallbackFileName);
6004
+ }
6005
+ if (data instanceof Blob) {
6006
+ return createConvertedOfficeBlobPreview(data, result.fileName || fallbackFileName);
6007
+ }
6008
+ return void 0;
6009
+ }
6010
+ function createConvertedOfficeBlobPreview(blob, fileName) {
6011
+ return {
6012
+ fileName,
6013
+ fileUrl: URL.createObjectURL(blob),
6014
+ fileSize: blob.size,
6015
+ mimeType: blob.type || "application/pdf",
6016
+ revokeUrlOnDestroy: true
6017
+ };
6018
+ }
6019
+ function stripFileExtension(fileName) {
6020
+ return fileName.replace(/\.[^.]+$/, "");
6021
+ }
5919
6022
  function createOfficeZoomController(panel, ctx) {
5920
6023
  const canZoom = Boolean(
5921
6024
  panel.querySelector(".ofv-docx-document, .ofv-sheet, .ofv-pptx-viewer > div, .ofv-document, .ofv-text-block, .ofv-slide")
@@ -6012,7 +6115,7 @@ async function renderDocx(panel, arrayBuffer) {
6012
6115
  useBase64URL: true
6013
6116
  });
6014
6117
  await normalizeDocxLayout(content, arrayBuffer);
6015
- const shouldUseTextboxFallback = await docxPreviewLooksBlank(content, arrayBuffer) || await docxPreviewMissesRichTextboxContent(content, arrayBuffer);
6118
+ const shouldUseTextboxFallback = await docxPreviewLooksBlank(content, arrayBuffer) || await docxPreviewMissesRichTextboxContent(content, arrayBuffer) || await docxShouldPreferTextboxLayoutFallback(arrayBuffer);
6016
6119
  if (shouldUseTextboxFallback) {
6017
6120
  disposeFit?.();
6018
6121
  styleContainer.remove();
@@ -6095,6 +6198,27 @@ async function docxHasRichTextboxContent(arrayBuffer) {
6095
6198
  return false;
6096
6199
  }
6097
6200
  }
6201
+ async function docxShouldPreferTextboxLayoutFallback(arrayBuffer) {
6202
+ try {
6203
+ const zip = await import_jszip3.default.loadAsync(arrayBuffer);
6204
+ const documentXml = await zip.file("word/document.xml")?.async("text");
6205
+ if (!documentXml || !/\btxbxContent\b/.test(documentXml)) {
6206
+ return false;
6207
+ }
6208
+ const blocks = extractDocxTextboxBlocks(documentXml);
6209
+ const meaningfulBlocks = blocks.filter((block) => block.paragraphs.length > 0);
6210
+ const sidebarBackgrounds = blocks.filter(
6211
+ (block) => block.paragraphs.length === 0 && block.fill && block.relativeV === "page" && block.x < 0 && block.width >= 120 && block.height >= 500
6212
+ );
6213
+ const pageAnchoredTextboxes = meaningfulBlocks.filter((block) => block.relativeV === "page");
6214
+ const paragraphAnchoredTextboxes = meaningfulBlocks.filter((block) => block.relativeV !== "page");
6215
+ const leftTextboxes = meaningfulBlocks.filter((block) => block.x < 0);
6216
+ const rightTextboxes = meaningfulBlocks.filter((block) => block.x >= 80);
6217
+ return sidebarBackgrounds.length >= 2 && meaningfulBlocks.length >= 8 && pageAnchoredTextboxes.length >= 4 && paragraphAnchoredTextboxes.length >= 2 && leftTextboxes.length >= 3 && rightTextboxes.length >= 3;
6218
+ } catch {
6219
+ return false;
6220
+ }
6221
+ }
6098
6222
  async function renderDocxContentFallback(container, arrayBuffer, options = {}) {
6099
6223
  if (options.showNote !== false) {
6100
6224
  const fallbackNote = document.createElement("div");
@@ -6546,6 +6670,10 @@ function createDocxTextboxBlockElement(block) {
6546
6670
  const paragraphs = normalizeDocxTextboxParagraphOrder(block);
6547
6671
  const [first, ...rest] = paragraphs;
6548
6672
  if (first) {
6673
+ const sectionKind = getDocxTextboxSectionKind(first);
6674
+ if (sectionKind) {
6675
+ section.classList.add(`ofv-docx-textbox-section-${sectionKind}`);
6676
+ }
6549
6677
  const heading = document.createElement("h3");
6550
6678
  heading.textContent = first;
6551
6679
  section.append(heading);
@@ -6558,6 +6686,28 @@ function createDocxTextboxBlockElement(block) {
6558
6686
  }
6559
6687
  return section;
6560
6688
  }
6689
+ function getDocxTextboxSectionKind(heading) {
6690
+ const text = normalizePreviewText(heading);
6691
+ if (text.includes("\u6559\u80B2\u80CC\u666F")) {
6692
+ return "education";
6693
+ }
6694
+ if (text.includes("\u4E13\u4E1A\u6280\u80FD")) {
6695
+ return "skills";
6696
+ }
6697
+ if (text.includes("\u5DE5\u4F5C\u7ECF\u5386")) {
6698
+ return "work";
6699
+ }
6700
+ if (text.includes("\u9879\u76EE\u7ECF\u9A8C")) {
6701
+ return "projects";
6702
+ }
6703
+ if (text.includes("\u81EA\u6211\u8BC4\u4EF7")) {
6704
+ return "summary";
6705
+ }
6706
+ if (text.includes("\u57FA\u672C\u4FE1\u606F")) {
6707
+ return "profile";
6708
+ }
6709
+ return "";
6710
+ }
6561
6711
  function estimateDocxTextboxBlockHeight(block) {
6562
6712
  return Math.max(block.height, 18 + block.paragraphs.length * 14);
6563
6713
  }
@@ -6601,13 +6751,57 @@ function repairDocxHeadingShapeAlignment(page) {
6601
6751
  if (!svg) {
6602
6752
  continue;
6603
6753
  }
6604
- const width = parseCssPixelValue(svg.getAttribute("width") || svg.style.width);
6754
+ const width = parseCssPixelValue(svg.style.width) || parseCssPixelValue(svg.getAttribute("width") || "");
6605
6755
  const marginLeft = parseCssPixelValue(svg.style.marginLeft);
6606
6756
  if (width < 300 || marginLeft < 28 || marginLeft > 44) {
6607
6757
  continue;
6608
6758
  }
6609
- svg.style.marginLeft = "48px";
6759
+ const textWidth = getDocxParagraphVisibleTextWidth(paragraph);
6760
+ svg.style.marginLeft = `${formatCssNumber(Math.max(48, marginLeft + textWidth * 0.68))}pt`;
6761
+ svg.style.marginTop = `${formatCssNumber(parseCssPixelValue(svg.style.marginTop) - 4)}pt`;
6762
+ normalizeDocxHeadingShapeFill(svg);
6763
+ repairDocxHeadingTextBackground(paragraph);
6764
+ }
6765
+ }
6766
+ function normalizeDocxHeadingShapeFill(svg) {
6767
+ const headingFill = "#3f4aa3";
6768
+ const fillNodes = svg.querySelectorAll("image[fill], rect[data-ofv-docx-shape-fill]");
6769
+ for (const node of fillNodes) {
6770
+ const fill = node.getAttribute("fill") || "";
6771
+ if (fill.toLowerCase() === "#38449a") {
6772
+ node.setAttribute("fill", headingFill);
6773
+ }
6774
+ }
6775
+ }
6776
+ function repairDocxHeadingTextBackground(paragraph) {
6777
+ const textSpans = Array.from(paragraph.querySelectorAll("span")).filter(
6778
+ (element) => normalizePreviewText(element.textContent || "")
6779
+ );
6780
+ const lastTextSpan = textSpans.at(-1);
6781
+ if (!lastTextSpan || !hasWhiteBackground(lastTextSpan)) {
6782
+ return;
6783
+ }
6784
+ lastTextSpan.style.paddingRight = "3pt";
6785
+ lastTextSpan.style.paddingTop = "2pt";
6786
+ lastTextSpan.style.paddingBottom = "2pt";
6787
+ lastTextSpan.style.boxDecorationBreak = "clone";
6788
+ }
6789
+ function hasWhiteBackground(element) {
6790
+ const background = element.style.backgroundColor.replace(/\s+/g, "").toLowerCase();
6791
+ return background === "white" || background === "#fff" || background === "#ffffff" || background === "rgb(255,255,255)";
6792
+ }
6793
+ function getDocxParagraphVisibleTextWidth(paragraph) {
6794
+ let textWidth = 0;
6795
+ for (const element of paragraph.querySelectorAll("span")) {
6796
+ if (!normalizePreviewText(element.textContent || "")) {
6797
+ continue;
6798
+ }
6799
+ textWidth += pxToPt(element.getBoundingClientRect().width);
6610
6800
  }
6801
+ return textWidth;
6802
+ }
6803
+ function pxToPt(value) {
6804
+ return value * 0.75;
6611
6805
  }
6612
6806
  function repairDocxListIndentAlignment(page) {
6613
6807
  for (const paragraph of page.querySelectorAll("p[class*='ofv-docx-num-']")) {