Import DOCX page setup into QWord
This commit is contained in:
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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" });
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user