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() {
|
async function saveDocx() {
|
||||||
try {
|
try {
|
||||||
const fileName = replaceFileExtension(document.title, ".docx");
|
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()) {
|
if (isDesktopOfficeBridgeAvailable()) {
|
||||||
await saveDesktopOfficeFile("docx", fileName, blob);
|
await saveDesktopOfficeFile("docx", fileName, blob);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -279,6 +279,17 @@ describe("wordOffice helpers", () => {
|
|||||||
expect(documentXml).toContain('<w:jc w:val="right"/>');
|
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 () => {
|
it("записывает внешние гиперссылки в DOCX relationships", async () => {
|
||||||
const blob = await exportDocxBlob(
|
const blob = await exportDocxBlob(
|
||||||
"Ссылки.docx",
|
"Ссылки.docx",
|
||||||
|
|||||||
+71
-2
@@ -1,4 +1,5 @@
|
|||||||
import { DOCX_MIME_TYPE, officeTitleFromFileName, readOfficeFileAsArrayBuffer } from "./fileHelpers";
|
import { DOCX_MIME_TYPE, officeTitleFromFileName, readOfficeFileAsArrayBuffer } from "./fileHelpers";
|
||||||
|
import type { WordPageMarginPreset, WordPageOrientation, WordPageSetup, WordPageSizeId } from "../types";
|
||||||
|
|
||||||
export interface ImportedWordDocument {
|
export interface ImportedWordDocument {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -116,6 +117,7 @@ type DocxModule = {
|
|||||||
Document: new (options: Record<string, unknown>) => unknown;
|
Document: new (options: Record<string, unknown>) => unknown;
|
||||||
HeadingLevel: Record<string, string>;
|
HeadingLevel: Record<string, string>;
|
||||||
LineRuleType?: Record<string, string>;
|
LineRuleType?: Record<string, string>;
|
||||||
|
PageOrientation?: Record<string, string>;
|
||||||
Packer: {
|
Packer: {
|
||||||
toBlob?: (document: unknown) => Promise<Blob>;
|
toBlob?: (document: unknown) => Promise<Blob>;
|
||||||
toBuffer?: (document: unknown) => Promise<ArrayBuffer | Uint8Array>;
|
toBuffer?: (document: unknown) => Promise<ArrayBuffer | Uint8Array>;
|
||||||
@@ -165,6 +167,16 @@ const wordImageRelationshipType = "http://schemas.openxmlformats.org/officeDocum
|
|||||||
const emptyDocxRelationshipTargets = { hyperlinks: {}, images: {} };
|
const emptyDocxRelationshipTargets = { hyperlinks: {}, images: {} };
|
||||||
const pageBreakMarker = "\f";
|
const pageBreakMarker = "\f";
|
||||||
const pageBreakHtml = '<span data-qoffice-page-break="true"></span>';
|
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> {
|
export async function importDocxFile(file: File): Promise<ImportedWordDocument> {
|
||||||
if (!file.name.toLowerCase().endsWith(".docx")) {
|
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 docx = await loadDocx();
|
||||||
const blocks = htmlToWordBlocks(html);
|
const blocks = htmlToWordBlocks(html);
|
||||||
const children = blocks.flatMap((block) => wordBlockToDocxChildren(block, docx));
|
const children = blocks.flatMap((block) => wordBlockToDocxChildren(block, docx));
|
||||||
const documentTitle = title.trim() || "Документ QWord";
|
const documentTitle = title.trim() || "Документ QWord";
|
||||||
|
const sectionProperties = wordSectionProperties(options.pageSetup, docx);
|
||||||
|
|
||||||
const document = new docx.Document({
|
const document = new docx.Document({
|
||||||
creator: "QOffice",
|
creator: "QOffice",
|
||||||
@@ -221,7 +234,7 @@ export async function exportDocxBlob(title: string, html: string): Promise<Blob>
|
|||||||
},
|
},
|
||||||
sections: [
|
sections: [
|
||||||
{
|
{
|
||||||
properties: {},
|
properties: sectionProperties,
|
||||||
children:
|
children:
|
||||||
children.length > 0
|
children.length > 0
|
||||||
? children
|
? children
|
||||||
@@ -246,6 +259,62 @@ export async function exportDocxBlob(title: string, html: string): Promise<Blob>
|
|||||||
throw new Error("Не удалось создать DOCX: библиотека docx не вернула поддерживаемый упаковщик.");
|
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) {
|
export function normalizeImportedDocxHtml(html: string) {
|
||||||
return html.trim();
|
return html.trim();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user