diff --git a/src/features/qword/QWord.tsx b/src/features/qword/QWord.tsx
index ee7f3b5..c1b099b 100644
--- a/src/features/qword/QWord.tsx
+++ b/src/features/qword/QWord.tsx
@@ -736,7 +736,7 @@ export function QWord({ document, zoom = 100, viewMode, onChange, onStatusChange
async function saveDocx() {
try {
const fileName = replaceFileExtension(document.title, ".docx");
- const blob = await exportDocxBlob(document.title, editorRef.current?.innerHTML ?? document.html);
+ const blob = await exportDocxBlob(document.title, editorRef.current?.innerHTML ?? document.html, { pageSetup: document.pageSetup });
if (isDesktopOfficeBridgeAvailable()) {
await saveDesktopOfficeFile("docx", fileName, blob);
return;
diff --git a/src/io/wordOffice.test.ts b/src/io/wordOffice.test.ts
index 6a337e8..15c75fb 100644
--- a/src/io/wordOffice.test.ts
+++ b/src/io/wordOffice.test.ts
@@ -279,6 +279,17 @@ describe("wordOffice helpers", () => {
expect(documentXml).toContain('');
});
+ it("writes QWord page setup to exported DOCX section properties", async () => {
+ const blob = await exportDocxBlob("Макет.docx", "
Landscape letter
", {
+ pageSetup: { size: "letter", orientation: "landscape", marginPreset: "narrow" }
+ });
+ const zip = await JSZip.loadAsync(await blob.arrayBuffer());
+ const documentXml = await zip.file("word/document.xml")?.async("string");
+
+ expect(documentXml).toMatch(/]*w:w="15840"[^>]*w:h="12240"[^>]*w:orient="landscape"[^>]*\/>/);
+ expect(documentXml).toMatch(/]*w:top="720"[^>]*w:right="720"[^>]*w:bottom="720"[^>]*w:left="720"[^>]*\/>/);
+ });
+
it("записывает внешние гиперссылки в DOCX relationships", async () => {
const blob = await exportDocxBlob(
"Ссылки.docx",
diff --git a/src/io/wordOffice.ts b/src/io/wordOffice.ts
index 77ab939..20b0542 100644
--- a/src/io/wordOffice.ts
+++ b/src/io/wordOffice.ts
@@ -1,4 +1,5 @@
import { DOCX_MIME_TYPE, officeTitleFromFileName, readOfficeFileAsArrayBuffer } from "./fileHelpers";
+import type { WordPageMarginPreset, WordPageOrientation, WordPageSetup, WordPageSizeId } from "../types";
export interface ImportedWordDocument {
title: string;
@@ -116,6 +117,7 @@ type DocxModule = {
Document: new (options: Record) => unknown;
HeadingLevel: Record;
LineRuleType?: Record;
+ PageOrientation?: Record;
Packer: {
toBlob?: (document: unknown) => Promise;
toBuffer?: (document: unknown) => Promise;
@@ -165,6 +167,16 @@ const wordImageRelationshipType = "http://schemas.openxmlformats.org/officeDocum
const emptyDocxRelationshipTargets = { hyperlinks: {}, images: {} };
const pageBreakMarker = "\f";
const pageBreakHtml = '';
+const defaultWordPageSetup: WordPageSetup = { size: "a4", orientation: "portrait", marginPreset: "normal" };
+const wordPageSizes: Record = {
+ a4: { width: 794, height: 1123 },
+ letter: { width: 816, height: 1056 }
+};
+const wordPageMargins: Record = {
+ normal: { top: 72, right: 86, bottom: 72, left: 86 },
+ narrow: { top: 48, right: 48, bottom: 48, left: 48 },
+ wide: { top: 92, right: 116, bottom: 92, left: 116 }
+};
export async function importDocxFile(file: File): Promise {
if (!file.name.toLowerCase().endsWith(".docx")) {
@@ -190,11 +202,12 @@ export async function importDocxFile(file: File): Promise
};
}
-export async function exportDocxBlob(title: string, html: string): Promise {
+export async function exportDocxBlob(title: string, html: string, options: { pageSetup?: WordPageSetup } = {}): Promise {
const docx = await loadDocx();
const blocks = htmlToWordBlocks(html);
const children = blocks.flatMap((block) => wordBlockToDocxChildren(block, docx));
const documentTitle = title.trim() || "Документ QWord";
+ const sectionProperties = wordSectionProperties(options.pageSetup, docx);
const document = new docx.Document({
creator: "QOffice",
@@ -221,7 +234,7 @@ export async function exportDocxBlob(title: string, html: string): Promise
},
sections: [
{
- properties: {},
+ properties: sectionProperties,
children:
children.length > 0
? children
@@ -246,6 +259,62 @@ export async function exportDocxBlob(title: string, html: string): Promise
throw new Error("Не удалось создать DOCX: библиотека docx не вернула поддерживаемый упаковщик.");
}
+function wordSectionProperties(pageSetup: WordPageSetup | undefined, docx: DocxModule) {
+ const normalizedPageSetup = normalizedWordPageSetup(pageSetup);
+ const size = wordPageSizeInTwips(normalizedPageSetup);
+ const margins = wordPageMargins[normalizedPageSetup.marginPreset];
+
+ return {
+ page: {
+ size: {
+ width: size.width,
+ height: size.height,
+ orientation:
+ normalizedPageSetup.orientation === "landscape"
+ ? (docx.PageOrientation?.LANDSCAPE ?? "landscape")
+ : (docx.PageOrientation?.PORTRAIT ?? "portrait")
+ },
+ margin: {
+ top: pxToTwips(margins.top),
+ right: pxToTwips(margins.right),
+ bottom: pxToTwips(margins.bottom),
+ left: pxToTwips(margins.left)
+ }
+ }
+ };
+}
+
+function normalizedWordPageSetup(pageSetup?: WordPageSetup): WordPageSetup {
+ const size = isWordPageSizeId(pageSetup?.size) ? pageSetup.size : defaultWordPageSetup.size;
+ const orientation = isWordPageOrientation(pageSetup?.orientation) ? pageSetup.orientation : defaultWordPageSetup.orientation;
+ const marginPreset = isWordPageMarginPreset(pageSetup?.marginPreset) ? pageSetup.marginPreset : defaultWordPageSetup.marginPreset;
+ return { size, orientation, marginPreset };
+}
+
+function wordPageSizeInTwips(pageSetup: WordPageSetup) {
+ const size = wordPageSizes[pageSetup.size];
+ return {
+ width: pxToTwips(size.width),
+ height: pxToTwips(size.height)
+ };
+}
+
+function pxToTwips(value: number) {
+ return Math.round(value * 15);
+}
+
+function isWordPageSizeId(value: unknown): value is WordPageSizeId {
+ return value === "a4" || value === "letter";
+}
+
+function isWordPageOrientation(value: unknown): value is WordPageOrientation {
+ return value === "portrait" || value === "landscape";
+}
+
+function isWordPageMarginPreset(value: unknown): value is WordPageMarginPreset {
+ return value === "normal" || value === "narrow" || value === "wide";
+}
+
export function normalizeImportedDocxHtml(html: string) {
return html.trim();
}