// A drawing the editor cannot author but must keep: a polygon, a polyline, a bezier // curve and a connector's elbow. Each imports as a box drawing the file's own outline, // and both exports write that outline back. import { describe, it, expect } from 'vitest'; import { zipSync, strToU8, unzipSync, strFromU8 } from 'fflate'; import { buildOdt } from '../../src/lib/export/odt'; import { importOdt } from '../../src/lib/import/odt'; import { buildDocx } from '../../src/lib/export/docx'; import { importDocx } from '../../src/lib/import/docx'; type N = any; const NS = 'xmlns:office="urn:oasis:names:tc:opendocument:xmlns:office:1.0"' + ' xmlns:style="urn:oasis:names:tc:opendocument:xmlns:style:1.0"' + ' xmlns:text="urn:oasis:names:tc:opendocument:xmlns:text:1.0"' + ' xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"' + ' xmlns:svg="urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0"' + ' xmlns:fo="urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0"'; function odt(body: string): Uint8Array { const content = `` + '' + '' + `${body}`; return zipSync({ 'mimetype': strToU8('application/vnd.oasis.opendocument.text'), 'content.xml': strToU8(content), 'styles.xml': strToU8(``), }); } const W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'; const VML = 'urn:schemas-microsoft-com:vml'; const docx = (body: string): Uint8Array => zipSync({ 'word/document.xml': strToU8(`` + `${body}`), }); const shape = (r: N): N => { const find = (n: N): N => n?.type === 'textBox' ? n : (n?.content ?? []).map(find).find(Boolean); return find((r.content as N)); }; const margins = { top: 2, bottom: 2, left: 2, right: 2 }; describe('a freeform drawing', () => { it('reads a polygon as its own closed outline', () => { const r = importOdt(odt('x')); expect(shape(r).attrs.shapePath).toBe('M 0 0 L 100 33.333 L 50 100 Z'); expect(shape(r).attrs.fillColor).toBe('#FFD320'); }); it('leaves a polyline open, so it is stroked and not filled', () => { const r = importOdt(odt('')); expect(shape(r).attrs.shapePath).toBe('M 0 0 L 25 100 L 100 50'); }); it('reads the relative bezier LibreOffice writes for a curve', () => { const r = importOdt(odt('')); expect(shape(r).attrs.shapePath).toBe('M 0 0 C 25 0 75 100 100 50 L 0 100 Z'); }); it("takes a connector's elbow, and the box its endpoints span", () => { const r = importOdt(odt('')); const a = shape(r).attrs; expect(a.shapePath).toBe('M 20 0 L 70 0 L 70 100 L 120 100'); // 5cm × 2cm at 96dpi. expect(a.width).toBe(189); expect(a.height).toBe(75.59); }); it('round-trips through ODF as a non-primitive custom shape', async () => { const doc: N = { type: 'doc', content: [ { type: 'paragraph', content: [{ type: 'text', text: 'before' }] }, { type: 'paragraph', content: [{ type: 'textBox', attrs: { width: 200, height: 150, shapePath: 'M 0 0 C 25 0 75 100 100 50 L 0 100 Z' }, content: [{ type: 'paragraph' }], }] }, ], }; const bytes = await buildOdt(doc, margins, 'portrait'); const xml = strFromU8(unzipSync(bytes)['content.xml']); expect(xml).toContain('draw:type="non-primitive"'); expect(xml).toContain('draw:enhanced-path="M 0 0 C 5400 0 16200 21600 21600 10800 L 0 21600 Z N"'); expect(shape(importOdt(bytes)).attrs.shapePath).toBe('M 0 0 C 25 0 75 100 100 50 L 0 100 Z'); }); it('round-trips through DOCX as a custGeom', async () => { const doc: N = { type: 'doc', content: [ { type: 'paragraph', content: [{ type: 'text', text: 'before' }] }, { type: 'paragraph', content: [{ type: 'textBox', attrs: { width: 200, height: 100, shapePath: 'M 0 0 L 100 50 L 0 100 Z' }, content: [{ type: 'paragraph' }], }] }, ], }; const bytes = await buildDocx(doc, margins, 'portrait'); const xml = strFromU8(unzipSync(bytes)['word/document.xml']); expect(xml).toContain(''); expect(shape(importDocx(bytes)).attrs.shapePath).toBe('M 0 0 L 100 50 L 0 100 Z'); }); it('reads the VML path LibreOffice writes into a .docx', () => { const body = '' + '' + ''; const r = importDocx(docx(body)); expect(shape(r).attrs.shapePath).toBe('M 0 0 L 100 33.333 L 50 100 L 0 0'); expect(shape(r).attrs.fillColor).toBe('#FFD320'); }); it('still drops a shape whose geometry is a formula', () => { const r = importOdt(odt('')); expect(shape(r)).toBeUndefined(); expect(r.warnings.some((w: string) => /shapes/i.test(w))).toBe(true); }); });