Import DOCX page setup into QWord

This commit is contained in:
Курнат Андрей
2026-06-04 21:58:24 +03:00
parent 217dafb760
commit 96aff5b76e
3 changed files with 121 additions and 4 deletions
+1 -1
View File
@@ -828,7 +828,7 @@ export function QWord({ document, zoom = 100, viewMode, onChange, onStatusChange
try {
const imported = await importDocxFile(file);
onChange({ ...document, title: imported.title, html: imported.html });
onChange({ ...document, title: imported.title, html: imported.html, ...(imported.pageSetup ? { pageSetup: imported.pageSetup } : {}) });
} catch (error) {
window.alert(error instanceof Error ? error.message : "Не удалось открыть DOCX.");
} finally {
+108 -3
View File
@@ -4,6 +4,7 @@ import type { WordPageMarginPreset, WordPageOrientation, WordPageSetup, WordPage
export interface ImportedWordDocument {
title: string;
html: string;
pageSetup?: WordPageSetup;
}
export interface WordInlineRun {
@@ -185,7 +186,7 @@ export async function importDocxFile(file: File): Promise<ImportedWordDocument>
const mammoth = await loadMammoth();
const arrayBuffer = await readOfficeFileAsArrayBuffer(file);
const [result, styledHtml] = await Promise.all([
const [result, styledHtml, pageSetup] = await Promise.all([
mammoth.convertToHtml(
{ arrayBuffer, buffer: arrayBuffer },
{
@@ -193,12 +194,14 @@ export async function importDocxFile(file: File): Promise<ImportedWordDocument>
...(mammoth.images?.dataUri ? { convertImage: mammoth.images.dataUri } : {})
}
),
styledDocxHtmlFromArrayBuffer(arrayBuffer)
styledDocxHtmlFromArrayBuffer(arrayBuffer),
docxPageSetupFromArrayBuffer(arrayBuffer)
]);
return {
title: officeTitleFromFileName(file.name, "Документ QWord.docx"),
html: normalizeImportedDocxHtml(styledHtml || result.value)
html: normalizeImportedDocxHtml(styledHtml || result.value),
...(pageSetup ? { pageSetup } : {})
};
}
@@ -1008,6 +1011,108 @@ async function styledDocxHtmlFromArrayBuffer(arrayBuffer: ArrayBuffer) {
}
}
async function docxPageSetupFromArrayBuffer(arrayBuffer: ArrayBuffer): Promise<WordPageSetup | undefined> {
try {
const [JSZip, XMLParser] = await Promise.all([loadJsZip(), loadXmlParser()]);
const zip = await JSZip.loadAsync(arrayBuffer);
const documentXml = await zip.file("word/document.xml")?.async("string");
if (!documentXml) {
return undefined;
}
const parser = new XMLParser(xmlParserOptions);
return docxPageSetupFromDocumentXml(parser.parse(documentXml));
} catch {
return undefined;
}
}
function docxPageSetupFromDocumentXml(documentXml: unknown): WordPageSetup | undefined {
const body = childRecord(childRecord(documentXml, "w:document"), "w:body");
const sectionProperties = lastDocxSectionProperties(body);
if (!hasXmlRecord(sectionProperties)) {
return undefined;
}
return docxPageSetupFromSectionProperties(sectionProperties);
}
function lastDocxSectionProperties(body: XmlRecord): XmlRecord {
const paragraphSectionProperties = toArray(asRecord(body)["w:p"])
.map((paragraph) => childRecord(childRecord(paragraph, "w:pPr"), "w:sectPr"))
.filter(hasXmlRecord);
const bodySectionProperties = childRecord(body, "w:sectPr");
const sections = hasXmlRecord(bodySectionProperties) ? [...paragraphSectionProperties, bodySectionProperties] : paragraphSectionProperties;
return sections[sections.length - 1] ?? {};
}
function docxPageSetupFromSectionProperties(sectionProperties: XmlRecord): WordPageSetup | undefined {
const pageSize = childRecord(sectionProperties, "w:pgSz");
const pageMargins = childRecord(sectionProperties, "w:pgMar");
const width = docxTwipsAttribute(pageSize, ["w:w"]);
const height = docxTwipsAttribute(pageSize, ["w:h"]);
if (!width || !height) {
return undefined;
}
const orientation = docxPageOrientation(pageSize, width, height);
const baseWidth = orientation === "landscape" ? height : width;
const baseHeight = orientation === "landscape" ? width : height;
const size = wordPageSizeIdFromTwips(baseWidth, baseHeight);
if (!size) {
return undefined;
}
const marginPreset = hasXmlRecord(pageMargins) ? (wordPageMarginPresetFromTwips(pageMargins) ?? defaultWordPageSetup.marginPreset) : defaultWordPageSetup.marginPreset;
return { size, orientation, marginPreset };
}
function docxPageOrientation(pageSize: XmlRecord, width: number, height: number): WordPageOrientation {
const value = docxAttribute(pageSize, "w:orient").trim().toLowerCase();
if (value === "landscape") {
return "landscape";
}
if (value === "portrait") {
return "portrait";
}
return width > height ? "landscape" : "portrait";
}
function wordPageSizeIdFromTwips(width: number, height: number): WordPageSizeId | undefined {
return (Object.keys(wordPageSizes) as WordPageSizeId[]).find((sizeId) => {
const size = wordPageSizes[sizeId];
return isCloseTwips(width, pxToTwips(size.width)) && isCloseTwips(height, pxToTwips(size.height));
});
}
function wordPageMarginPresetFromTwips(pageMargins: XmlRecord): WordPageMarginPreset | undefined {
const top = docxTwipsAttribute(pageMargins, ["w:top"]);
const right = docxTwipsAttribute(pageMargins, ["w:right"]);
const bottom = docxTwipsAttribute(pageMargins, ["w:bottom"]);
const left = docxTwipsAttribute(pageMargins, ["w:left"]);
if (!top || !right || !bottom || !left) {
return undefined;
}
return (Object.keys(wordPageMargins) as WordPageMarginPreset[]).find((preset) => {
const margins = wordPageMargins[preset];
return (
isCloseTwips(top, pxToTwips(margins.top)) &&
isCloseTwips(right, pxToTwips(margins.right)) &&
isCloseTwips(bottom, pxToTwips(margins.bottom)) &&
isCloseTwips(left, pxToTwips(margins.left))
);
});
}
function isCloseTwips(actual: number, expected: number) {
return Math.abs(actual - expected) <= 40;
}
function hasXmlRecord(record: XmlRecord) {
return Object.keys(record).length > 0;
}
function isSimpleStyledDocxDocument(documentXml: string) {
return !/<w:(pict|sdt)\b/i.test(documentXml);
}
+12
View File
@@ -449,4 +449,16 @@ describe("wordOffice DOCX import", () => {
{ type: "paragraph", text: "Exact line height", runs: [{ text: "Exact line height" }], spacing: { line: 480, lineRule: "exact" } }
]);
});
it("imports DOCX page setup from OOXML section properties", async () => {
const blob = await exportDocxBlob("PageSetup.docx", "<p>Layout</p>", {
pageSetup: { size: "letter", orientation: "landscape", marginPreset: "narrow" }
});
const file = new File([blob], "PageSetup.docx", {
type: "application/vnd.openxmlformats-officedocument.wordprocessingml.document"
});
const imported = await importDocxFile(file);
expect(imported.pageSetup).toEqual({ size: "letter", orientation: "landscape", marginPreset: "narrow" });
});
});