mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-08 00:18:12 +08:00
fix(document-preview): strip WPS DISPIMG formula from xlsx before preview (#17912)
This commit is contained in:
60
web/package-lock.json
generated
60
web/package-lock.json
generated
@@ -84,6 +84,7 @@
|
||||
"js-base64": "^3.7.5",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"jsoneditor": "^10.4.2",
|
||||
"jszip": "^3.10.1",
|
||||
"lexical": "^0.45.0",
|
||||
"lodash": "^4.18.1",
|
||||
"lucide-react": "^1.7.0",
|
||||
@@ -144,6 +145,7 @@
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/jszip": "^3.4.0",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/react": "^18.0.33",
|
||||
@@ -3286,9 +3288,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3306,9 +3305,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3326,9 +3322,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3346,9 +3339,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3366,9 +3356,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3386,9 +3373,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3406,9 +3390,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3426,9 +3407,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3633,9 +3611,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3653,9 +3628,6 @@
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3673,9 +3645,6 @@
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3693,9 +3662,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3713,9 +3679,6 @@
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3733,9 +3696,6 @@
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3753,9 +3713,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -3773,9 +3730,6 @@
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
@@ -9155,6 +9109,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/jszip": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmmirror.com/@types/jszip/-/jszip-3.4.0.tgz",
|
||||
"integrity": "sha512-GFHqtQQP3R4NNuvZH3hNCYD0NbyBZ42bkN7kO3NDrU/SnvIZWMS8Bp38XCsRKBT5BXvgm0y1zqpZWp/ZkRzBzg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"jszip": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/katex": {
|
||||
"version": "0.16.7",
|
||||
"resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.16.7.tgz",
|
||||
|
||||
@@ -94,6 +94,7 @@
|
||||
"js-base64": "^3.7.5",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"jsoneditor": "^10.4.2",
|
||||
"jszip": "^3.10.1",
|
||||
"lexical": "^0.45.0",
|
||||
"lodash": "^4.18.1",
|
||||
"lucide-react": "^1.7.0",
|
||||
@@ -154,6 +155,7 @@
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@types/dompurify": "^3.0.5",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/jszip": "^3.4.0",
|
||||
"@types/lodash": "^4.14.202",
|
||||
"@types/node": "^24.3.0",
|
||||
"@types/react": "^18.0.33",
|
||||
@@ -165,8 +167,6 @@
|
||||
"@types/webpack-env": "^1.18.4",
|
||||
"@vitejs/plugin-react": "^5.1.2",
|
||||
"autoprefixer": "^10.4.21",
|
||||
"oxfmt": "^0.60.0",
|
||||
"oxlint": "^1.75.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"esbuild-jest": "^0.5.0",
|
||||
"html-loader": "^5.1.0",
|
||||
@@ -174,6 +174,8 @@
|
||||
"jest": "^29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"less": "^4.4.2",
|
||||
"oxfmt": "^0.60.0",
|
||||
"oxlint": "^1.75.0",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-loader": "^8.2.0",
|
||||
"react-dev-inspector": "^2.0.1",
|
||||
|
||||
@@ -4,8 +4,9 @@ import { useGetPipelineResultSearchParams } from '@/pages/dataflow-result/hooks'
|
||||
import api, { restAPIv1 } from '@/utils/api';
|
||||
import { getAuthorization } from '@/utils/authorization-util';
|
||||
import jsPreviewExcel from '@js-preview/excel';
|
||||
import { useSize } from 'ahooks';
|
||||
import { useDebounceFn, useSize } from 'ahooks';
|
||||
import axios from 'axios';
|
||||
import JSZip from 'jszip';
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
// ZIP file header bytes "PK"
|
||||
@@ -114,36 +115,152 @@ export const useFetchDocument = () => {
|
||||
return { fetchDocument };
|
||||
};
|
||||
|
||||
/**
|
||||
* WPS spreadsheets embed images in cells via the proprietary DISPIMG formula
|
||||
* (stored in xl/cellimages.xml). @js-preview/excel cannot evaluate this
|
||||
* formula and crashes with "Cannot read properties of undefined (reading
|
||||
* 'render')". This function strips DISPIMG formulas from worksheet XML,
|
||||
* replacing them with a text placeholder so the rest of the sheet renders.
|
||||
*/
|
||||
async function stripWpsDispImg(data: ArrayBuffer): Promise<ArrayBuffer> {
|
||||
const zip = await JSZip.loadAsync(data);
|
||||
|
||||
const worksheetPaths = Object.keys(zip.files).filter((path) =>
|
||||
/^xl\/worksheets\/sheet\d+\.xml$/.test(path),
|
||||
);
|
||||
|
||||
let modified = false;
|
||||
for (const path of worksheetPaths) {
|
||||
const file = zip.file(path);
|
||||
if (!file) continue;
|
||||
const xml = await file.async('string');
|
||||
if (!xml.includes('DISPIMG')) continue;
|
||||
|
||||
modified = true;
|
||||
let cleaned = xml;
|
||||
// Remove <f> formula tags that reference DISPIMG
|
||||
cleaned = cleaned.replace(
|
||||
/<f\b[^>]*>[\s\S]*?DISPIMG[\s\S]*?<\/f>/g,
|
||||
'',
|
||||
);
|
||||
// Replace cached <v> values that reference DISPIMG with a placeholder
|
||||
cleaned = cleaned.replace(
|
||||
/<v>[^<]*DISPIMG[^<]*<\/v>/g,
|
||||
'<v>[图片]</v>',
|
||||
);
|
||||
zip.file(path, cleaned);
|
||||
}
|
||||
|
||||
if (!modified) return data;
|
||||
return zip.generateAsync({
|
||||
type: 'arraybuffer',
|
||||
compression: 'DEFLATE',
|
||||
});
|
||||
}
|
||||
|
||||
export const useFetchExcel = (filePath: string) => {
|
||||
const [status, setStatus] = useState(true);
|
||||
const { fetchDocument } = useFetchDocument();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [containerEl, setContainerEl] = useState<HTMLDivElement | null>(null);
|
||||
const size = useSize(containerEl);
|
||||
const { error } = useCatchError(filePath);
|
||||
|
||||
const fetchDocumentAsync = useCallback(async () => {
|
||||
let myExcelPreviewer;
|
||||
if (containerRef.current) {
|
||||
myExcelPreviewer = jsPreviewExcel.init(containerRef.current);
|
||||
const previewerRef = useRef<ReturnType<typeof jsPreviewExcel.init> | null>(
|
||||
null,
|
||||
);
|
||||
// Cache the fetched ArrayBuffer so we don't re-fetch on every resize.
|
||||
const dataRef = useRef<ArrayBuffer | null>(null);
|
||||
const [dataReady, setDataReady] = useState(false);
|
||||
|
||||
// @js-preview/excel reads the container's width/height at init time and
|
||||
// exposes no public resize method, so the only way to make the spreadsheet
|
||||
// track container size changes is to destroy + re-init the previewer.
|
||||
const renderPreview = useCallback(async () => {
|
||||
if (!containerEl || !dataRef.current) return;
|
||||
|
||||
if (previewerRef.current) {
|
||||
previewerRef.current.destroy();
|
||||
previewerRef.current = null;
|
||||
}
|
||||
const jsonFile = await fetchDocument(filePath);
|
||||
myExcelPreviewer
|
||||
?.preview(jsonFile.data)
|
||||
.then(() => {
|
||||
setStatus(true);
|
||||
})
|
||||
.catch((e) => {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.warn('failed', e);
|
||||
myExcelPreviewer.destroy();
|
||||
setStatus(false);
|
||||
});
|
||||
|
||||
const previewer = jsPreviewExcel.init(containerEl);
|
||||
previewerRef.current = previewer;
|
||||
|
||||
try {
|
||||
await previewer.preview(dataRef.current);
|
||||
setStatus(true);
|
||||
} catch (e) {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.warn('failed', e);
|
||||
previewer.destroy();
|
||||
previewerRef.current = null;
|
||||
setStatus(false);
|
||||
}
|
||||
}, [containerEl]);
|
||||
|
||||
// Leading edge renders immediately on first data/size, trailing edge
|
||||
// collapses rapid resize bursts into a single re-render.
|
||||
const { run: debouncedRender } = useDebounceFn(renderPreview, {
|
||||
wait: 150,
|
||||
leading: true,
|
||||
trailing: true,
|
||||
});
|
||||
|
||||
// Fetch the file once per url. On url change, drop cached data and the
|
||||
// existing previewer so the next render starts from a clean state.
|
||||
useEffect(() => {
|
||||
if (!filePath) return;
|
||||
|
||||
setDataReady(false);
|
||||
setStatus(true);
|
||||
if (previewerRef.current) {
|
||||
previewerRef.current.destroy();
|
||||
previewerRef.current = null;
|
||||
}
|
||||
dataRef.current = null;
|
||||
|
||||
let cancelled = false;
|
||||
(async () => {
|
||||
try {
|
||||
const jsonFile = await fetchDocument(filePath);
|
||||
if (cancelled) return;
|
||||
try {
|
||||
dataRef.current = await stripWpsDispImg(jsonFile.data);
|
||||
} catch {
|
||||
// Not a valid ZIP or preprocessing failed — use original data
|
||||
dataRef.current = jsonFile.data;
|
||||
}
|
||||
setDataReady(true);
|
||||
} catch (e) {
|
||||
if (!cancelled) {
|
||||
// oxlint-disable-next-line no-console
|
||||
console.warn('failed to fetch', e);
|
||||
setStatus(false);
|
||||
}
|
||||
}
|
||||
})();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [filePath, fetchDocument]);
|
||||
|
||||
// Initial render + debounced re-render on container resize.
|
||||
useEffect(() => {
|
||||
fetchDocumentAsync();
|
||||
}, [fetchDocumentAsync]);
|
||||
if (!dataReady || !containerEl) return;
|
||||
debouncedRender();
|
||||
}, [dataReady, containerEl, size?.width, size?.height, debouncedRender]);
|
||||
|
||||
return { status, containerRef, error };
|
||||
// Tear down the previewer on unmount.
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (previewerRef.current) {
|
||||
previewerRef.current.destroy();
|
||||
previewerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { status, containerRef: setContainerEl, error };
|
||||
};
|
||||
|
||||
export const useCatchDocumentError = (url: string) => {
|
||||
|
||||
Reference in New Issue
Block a user