mirror of
https://github.com/infiniflow/ragflow.git
synced 2026-08-04 23:00:30 +08:00
Fix overlapping CJK text lines in docx file preview (#17693)
This commit is contained in:
@@ -3,6 +3,8 @@ import { Spin } from '@/components/ui/spin';
|
||||
import request from '@/utils/request';
|
||||
import {
|
||||
DocxEditorViewer,
|
||||
packageToArrayBuffer,
|
||||
parseDocx,
|
||||
useDocxEditor,
|
||||
useDocxPageLayout,
|
||||
} from '@extend-ai/react-docx';
|
||||
@@ -21,6 +23,120 @@ interface DocPreviewerProps {
|
||||
url: string;
|
||||
}
|
||||
|
||||
// @extend-ai/react-docx renders paragraphs without explicit line spacing at
|
||||
// 0.88x the font size, which makes CJK glyph lines overlap. Word renders such
|
||||
// paragraphs with the font's natural line height (~1.3x for CJK fonts).
|
||||
// Inject a docDefaults-level line spacing (1.3 lines) so paragraphs that do
|
||||
// not define their own line spacing get a consistent, readable line pitch.
|
||||
// Paragraphs or styles with explicit line spacing are left untouched.
|
||||
const DEFAULT_LINE_SPACING_TWIPS = 312; // 1.3 lines (240 twips per line)
|
||||
const DEFAULT_LINE_SPACING_TAG = `<w:spacing w:line="${DEFAULT_LINE_SPACING_TWIPS}" w:lineRule="auto"/>`;
|
||||
|
||||
const DEFAULT_LINE_SPACING_PPR_DEFAULT = `<w:pPrDefault><w:pPr>${DEFAULT_LINE_SPACING_TAG}</w:pPr></w:pPrDefault>`;
|
||||
|
||||
// Ensure the docDefaults section of word/styles.xml defines a default line
|
||||
// spacing. Returns the original XML when a default already exists.
|
||||
const ensureDefaultLineSpacing = (stylesXml: string): string => {
|
||||
const docDefaults = stylesXml.match(
|
||||
/<w:docDefaults\b[^>]*>[\s\S]*?<\/w:docDefaults>/i,
|
||||
)?.[0];
|
||||
if (!docDefaults) {
|
||||
const selfClosing = stylesXml.match(/<w:docDefaults\b[^>]*\/>/i)?.[0];
|
||||
if (selfClosing) {
|
||||
return stylesXml.replace(
|
||||
selfClosing,
|
||||
`<w:docDefaults>${DEFAULT_LINE_SPACING_PPR_DEFAULT}</w:docDefaults>`,
|
||||
);
|
||||
}
|
||||
const stylesOpen = stylesXml.match(/<w:styles\b[^>]*>/i)?.[0];
|
||||
if (!stylesOpen) return stylesXml;
|
||||
return stylesXml.replace(
|
||||
stylesOpen,
|
||||
`${stylesOpen}<w:docDefaults>${DEFAULT_LINE_SPACING_PPR_DEFAULT}</w:docDefaults>`,
|
||||
);
|
||||
}
|
||||
|
||||
const replaceDocDefaults = (next: string) =>
|
||||
stylesXml.replace(docDefaults, next);
|
||||
|
||||
const pprDefault = docDefaults.match(
|
||||
/<w:pPrDefault\b[^>]*>[\s\S]*?<\/w:pPrDefault>/i,
|
||||
)?.[0];
|
||||
if (!pprDefault) {
|
||||
const selfClosing = docDefaults.match(/<w:pPrDefault\b[^>]*\/>/i)?.[0];
|
||||
if (selfClosing) {
|
||||
return replaceDocDefaults(
|
||||
docDefaults.replace(selfClosing, DEFAULT_LINE_SPACING_PPR_DEFAULT),
|
||||
);
|
||||
}
|
||||
const docDefaultsOpen = docDefaults.match(/<w:docDefaults\b[^>]*>/i)?.[0];
|
||||
if (!docDefaultsOpen) return stylesXml;
|
||||
return replaceDocDefaults(
|
||||
docDefaults.replace(
|
||||
docDefaultsOpen,
|
||||
`${docDefaultsOpen}${DEFAULT_LINE_SPACING_PPR_DEFAULT}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const replacePprDefault = (next: string) =>
|
||||
replaceDocDefaults(docDefaults.replace(pprDefault, next));
|
||||
|
||||
const spacingTag = pprDefault.match(/<w:spacing\b[^>]*?\/?>/i)?.[0];
|
||||
if (spacingTag) {
|
||||
// A default line spacing already exists; respect the document.
|
||||
if (/\bw:line\s*=/i.test(spacingTag)) return stylesXml;
|
||||
let tag = spacingTag.replace(/\s*\/?>$/, '');
|
||||
tag += ` w:line="${DEFAULT_LINE_SPACING_TWIPS}"`;
|
||||
if (!/\bw:lineRule\s*=/i.test(spacingTag)) {
|
||||
tag += ' w:lineRule="auto"';
|
||||
}
|
||||
return replacePprDefault(pprDefault.replace(spacingTag, `${tag}/>`));
|
||||
}
|
||||
|
||||
const pprTag = pprDefault.match(/<w:pPr\b[^>]*>/i)?.[0];
|
||||
if (pprTag) {
|
||||
if (pprTag.endsWith('/>')) {
|
||||
return replacePprDefault(
|
||||
pprDefault.replace(
|
||||
pprTag,
|
||||
`<w:pPr>${DEFAULT_LINE_SPACING_TAG}</w:pPr>`,
|
||||
),
|
||||
);
|
||||
}
|
||||
return replacePprDefault(
|
||||
pprDefault.replace(pprTag, `${pprTag}${DEFAULT_LINE_SPACING_TAG}`),
|
||||
);
|
||||
}
|
||||
const pprDefaultOpen = pprDefault.match(/<w:pPrDefault\b[^>]*>/i)?.[0];
|
||||
if (!pprDefaultOpen) return stylesXml;
|
||||
return replacePprDefault(
|
||||
pprDefault.replace(
|
||||
pprDefaultOpen,
|
||||
`${pprDefaultOpen}<w:pPr>${DEFAULT_LINE_SPACING_TAG}</w:pPr>`,
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
// Repack the docx with a default line spacing so the preview renders
|
||||
// consistent line pitch. Falls back to the original blob on any failure.
|
||||
const normalizeDocxLineSpacing = async (blob: Blob): Promise<Blob> => {
|
||||
try {
|
||||
const pkg = await parseDocx(await blob.arrayBuffer());
|
||||
const stylesPart = pkg.parts.get('word/styles.xml');
|
||||
if (!stylesPart) return blob;
|
||||
const patched = ensureDefaultLineSpacing(stylesPart.content);
|
||||
if (patched === stylesPart.content) return blob;
|
||||
stylesPart.content = patched;
|
||||
return new Blob([packageToArrayBuffer(pkg)], {
|
||||
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn('Failed to normalize docx line spacing:', error);
|
||||
return blob;
|
||||
}
|
||||
};
|
||||
|
||||
// Word document preview component.
|
||||
// Uses @extend-ai/react-docx for canvas-based page-level rendering.
|
||||
// Falls back to an unsupported notice for legacy .doc (non-ZIP) payloads.
|
||||
@@ -87,7 +203,11 @@ export const DocPreviewer: React.FC<DocPreviewerProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const file = new File([blob], 'document.docx', {
|
||||
const normalizedBlob = await normalizeDocxLineSpacing(blob);
|
||||
|
||||
if (cancelledRef.current) return;
|
||||
|
||||
const file = new File([normalizedBlob], 'document.docx', {
|
||||
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user