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 {
|
try {
|
||||||
const imported = await importDocxFile(file);
|
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) {
|
} catch (error) {
|
||||||
window.alert(error instanceof Error ? error.message : "Не удалось открыть DOCX.");
|
window.alert(error instanceof Error ? error.message : "Не удалось открыть DOCX.");
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
+108
-3
@@ -4,6 +4,7 @@ import type { WordPageMarginPreset, WordPageOrientation, WordPageSetup, WordPage
|
|||||||
export interface ImportedWordDocument {
|
export interface ImportedWordDocument {
|
||||||
title: string;
|
title: string;
|
||||||
html: string;
|
html: string;
|
||||||
|
pageSetup?: WordPageSetup;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WordInlineRun {
|
export interface WordInlineRun {
|
||||||
@@ -185,7 +186,7 @@ export async function importDocxFile(file: File): Promise<ImportedWordDocument>
|
|||||||
|
|
||||||
const mammoth = await loadMammoth();
|
const mammoth = await loadMammoth();
|
||||||
const arrayBuffer = await readOfficeFileAsArrayBuffer(file);
|
const arrayBuffer = await readOfficeFileAsArrayBuffer(file);
|
||||||
const [result, styledHtml] = await Promise.all([
|
const [result, styledHtml, pageSetup] = await Promise.all([
|
||||||
mammoth.convertToHtml(
|
mammoth.convertToHtml(
|
||||||
{ arrayBuffer, buffer: arrayBuffer },
|
{ arrayBuffer, buffer: arrayBuffer },
|
||||||
{
|
{
|
||||||
@@ -193,12 +194,14 @@ export async function importDocxFile(file: File): Promise<ImportedWordDocument>
|
|||||||
...(mammoth.images?.dataUri ? { convertImage: mammoth.images.dataUri } : {})
|
...(mammoth.images?.dataUri ? { convertImage: mammoth.images.dataUri } : {})
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
styledDocxHtmlFromArrayBuffer(arrayBuffer)
|
styledDocxHtmlFromArrayBuffer(arrayBuffer),
|
||||||
|
docxPageSetupFromArrayBuffer(arrayBuffer)
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: officeTitleFromFileName(file.name, "Документ QWord.docx"),
|
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) {
|
function isSimpleStyledDocxDocument(documentXml: string) {
|
||||||
return !/<w:(pict|sdt)\b/i.test(documentXml);
|
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" } }
|
{ 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