diff --git a/packages/super-editor/src/core/DocxZipper.js b/packages/super-editor/src/core/DocxZipper.js
index 018ceb96c7..42bd3b4f22 100644
--- a/packages/super-editor/src/core/DocxZipper.js
+++ b/packages/super-editor/src/core/DocxZipper.js
@@ -262,7 +262,7 @@ class DocxZipper {
return zip;
}
- async updateZip({ docx, updatedDocs, originalDocxFile, media, fonts, isHeadless }) {
+ async updateZip({ docx, updatedDocs, originalDocxFile, media, fonts, isHeadless, compression = 'DEFLATE' }) {
// We use a different re-zip process if we have the original docx vs the docx xml metadata
let zip;
@@ -274,7 +274,11 @@ class DocxZipper {
// If we are headless we don't have 'blob' support, so export as 'nodebuffer'
const exportType = isHeadless ? 'nodebuffer' : 'blob';
- return await zip.generateAsync({ type: exportType });
+ return await zip.generateAsync({
+ type: exportType,
+ compression,
+ compressionOptions: compression === 'DEFLATE' ? { level: 6 } : undefined,
+ });
}
/**
diff --git a/packages/super-editor/src/core/DocxZipper.test.js b/packages/super-editor/src/core/DocxZipper.test.js
index b004bfcff4..02cd3fef08 100644
--- a/packages/super-editor/src/core/DocxZipper.test.js
+++ b/packages/super-editor/src/core/DocxZipper.test.js
@@ -109,6 +109,97 @@ describe('DocxZipper - UTF-16 XML handling', () => {
});
});
+describe('DocxZipper - updateZip compression', () => {
+ it('uses DEFLATE compression by default', async () => {
+ const zipper = new DocxZipper();
+
+ const contentTypes = `
+
+
+
+
+ `;
+
+ const documentXml = `
+
+ ${'Hello world. '.repeat(100)}
+ `;
+
+ const docx = [
+ { name: '[Content_Types].xml', content: contentTypes },
+ { name: 'word/document.xml', content: documentXml },
+ ];
+
+ const result = await zipper.updateZip({
+ docx,
+ updatedDocs: {},
+ media: {},
+ fonts: {},
+ isHeadless: true,
+ });
+
+ // Verify the output is compressed by checking DEFLATE produces smaller output than STORE
+ const storeResult = await new DocxZipper().updateZip({
+ docx,
+ updatedDocs: {},
+ media: {},
+ fonts: {},
+ isHeadless: true,
+ compression: 'STORE',
+ });
+
+ expect(result.length).toBeLessThan(storeResult.length);
+
+ // Verify the compressed output is a valid zip that can be read back
+ const readBack = await new JSZip().loadAsync(result);
+ const docXml = await readBack.file('word/document.xml').async('string');
+ expect(docXml).toContain('Hello world.');
+ });
+
+ it('respects STORE compression when explicitly requested', async () => {
+ const zipper = new DocxZipper();
+
+ const contentTypes = `
+
+
+
+
+ `;
+
+ const documentXml = `
+
+ ${'Hello world. '.repeat(100)}
+ `;
+
+ const docx = [
+ { name: '[Content_Types].xml', content: contentTypes },
+ { name: 'word/document.xml', content: documentXml },
+ ];
+
+ const result = await zipper.updateZip({
+ docx,
+ updatedDocs: {},
+ media: {},
+ fonts: {},
+ isHeadless: true,
+ compression: 'STORE',
+ });
+
+ // STORE should produce output roughly the size of the uncompressed content
+ // (plus ZIP overhead), so it should be larger than DEFLATE
+ const deflateResult = await new DocxZipper().updateZip({
+ docx,
+ updatedDocs: {},
+ media: {},
+ fonts: {},
+ isHeadless: true,
+ compression: 'DEFLATE',
+ });
+
+ expect(result.length).toBeGreaterThan(deflateResult.length);
+ });
+});
+
describe('DocxZipper - updateContentTypes', () => {
it('adds header/footer overrides for newly added parts', async () => {
const zipper = new DocxZipper();
diff --git a/packages/super-editor/src/core/Editor.ts b/packages/super-editor/src/core/Editor.ts
index bca42f0eb3..133865332a 100644
--- a/packages/super-editor/src/core/Editor.ts
+++ b/packages/super-editor/src/core/Editor.ts
@@ -138,6 +138,9 @@ export interface SaveOptions {
/** Highlight color for fields */
fieldsHighlightColor?: string | null;
+
+ /** ZIP compression method for docx export. Defaults to 'DEFLATE'. Use 'STORE' for faster exports without compression. */
+ compression?: 'DEFLATE' | 'STORE';
}
/**
@@ -2484,6 +2487,7 @@ export class Editor extends EventEmitter {
comments,
getUpdatedDocs = false,
fieldsHighlightColor = null,
+ compression,
}: {
isFinalDoc?: boolean;
commentsType?: string;
@@ -2492,6 +2496,7 @@ export class Editor extends EventEmitter {
comments?: Comment[];
getUpdatedDocs?: boolean;
fieldsHighlightColor?: string | null;
+ compression?: 'DEFLATE' | 'STORE';
} = {}): Promise | ProseMirrorJSON | string | undefined> {
try {
// Use provided comments, or fall back to imported comments from converter
@@ -2623,6 +2628,7 @@ export class Editor extends EventEmitter {
media,
fonts: this.options.fonts,
isHeadless: this.options.isHeadless,
+ compression,
});
return result;
@@ -2893,6 +2899,7 @@ export class Editor extends EventEmitter {
commentsType: options?.commentsType,
comments: options?.comments,
fieldsHighlightColor: options?.fieldsHighlightColor,
+ compression: options?.compression,
});
return result as Blob | Buffer;