Export QWord page setup to DOCX
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -279,6 +279,17 @@ describe("wordOffice helpers", () => {
|
||||
expect(documentXml).toContain('<w:jc w:val="right"/>');
|
||||
});
|
||||
|
||||
it("writes QWord page setup to exported DOCX section properties", async () => {
|
||||
const blob = await exportDocxBlob("Макет.docx", "<p>Landscape letter</p>", {
|
||||
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:pgSz\b[^>]*w:w="15840"[^>]*w:h="12240"[^>]*w:orient="landscape"[^>]*\/>/);
|
||||
expect(documentXml).toMatch(/<w:pgMar\b[^>]*w:top="720"[^>]*w:right="720"[^>]*w:bottom="720"[^>]*w:left="720"[^>]*\/>/);
|
||||
});
|
||||
|
||||
it("записывает внешние гиперссылки в DOCX relationships", async () => {
|
||||
const blob = await exportDocxBlob(
|
||||
"Ссылки.docx",
|
||||
|
||||
+71
-2
@@ -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<string, unknown>) => unknown;
|
||||
HeadingLevel: Record<string, string>;
|
||||
LineRuleType?: Record<string, string>;
|
||||
PageOrientation?: Record<string, string>;
|
||||
Packer: {
|
||||
toBlob?: (document: unknown) => Promise<Blob>;
|
||||
toBuffer?: (document: unknown) => Promise<ArrayBuffer | Uint8Array>;
|
||||
@@ -165,6 +167,16 @@ const wordImageRelationshipType = "http://schemas.openxmlformats.org/officeDocum
|
||||
const emptyDocxRelationshipTargets = { hyperlinks: {}, images: {} };
|
||||
const pageBreakMarker = "\f";
|
||||
const pageBreakHtml = '<span data-qoffice-page-break="true"></span>';
|
||||
const defaultWordPageSetup: WordPageSetup = { size: "a4", orientation: "portrait", marginPreset: "normal" };
|
||||
const wordPageSizes: Record<WordPageSizeId, { width: number; height: number }> = {
|
||||
a4: { width: 794, height: 1123 },
|
||||
letter: { width: 816, height: 1056 }
|
||||
};
|
||||
const wordPageMargins: Record<WordPageMarginPreset, { top: number; right: number; bottom: number; left: number }> = {
|
||||
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<ImportedWordDocument> {
|
||||
if (!file.name.toLowerCase().endsWith(".docx")) {
|
||||
@@ -190,11 +202,12 @@ export async function importDocxFile(file: File): Promise<ImportedWordDocument>
|
||||
};
|
||||
}
|
||||
|
||||
export async function exportDocxBlob(title: string, html: string): Promise<Blob> {
|
||||
export async function exportDocxBlob(title: string, html: string, options: { pageSetup?: WordPageSetup } = {}): Promise<Blob> {
|
||||
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<Blob>
|
||||
},
|
||||
sections: [
|
||||
{
|
||||
properties: {},
|
||||
properties: sectionProperties,
|
||||
children:
|
||||
children.length > 0
|
||||
? children
|
||||
@@ -246,6 +259,62 @@ export async function exportDocxBlob(title: string, html: string): Promise<Blob>
|
||||
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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user