@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 +38 -0
- package/dist/index.cjs +201 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +21 -2
- package/dist/index.d.ts +21 -2
- package/dist/index.js +201 -7
- package/dist/index.js.map +1 -1
- package/dist/style.css +56 -23
- package/package.json +1 -1
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
|
-
|
|
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)
|
|
5909
|
+
return delegatedInstance?.canCommand?.(command) || controller?.canCommand(command) || false;
|
|
5906
5910
|
},
|
|
5907
5911
|
command(command) {
|
|
5908
|
-
return controller?.command(command)
|
|
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") ||
|
|
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
|
-
|
|
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-']")) {
|