Skip to content

Commit 1e5681b

Browse files
authored
fix: normalize positioned text box offsets (#511)
1 parent 460a819 commit 1e5681b

3 files changed

Lines changed: 74 additions & 2 deletions

File tree

.changeset/clean-boxes-rest.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@stll/folio-core": patch
3+
---
4+
5+
Normalize omitted offsets on positioned legacy text boxes.

packages/core/src/docx/paragraphTextBoxEnrichment.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -485,11 +485,11 @@ const parseVmlTextBoxShape = (
485485
shape.position = {
486486
horizontal: {
487487
relativeTo: horizontalRelativeTo(style["mso-position-horizontal-relative"]),
488-
...(left === undefined ? {} : { posOffset: pixelsToEmu(left) }),
488+
posOffset: pixelsToEmu(left ?? 0),
489489
},
490490
vertical: {
491491
relativeTo: verticalRelativeTo(style["mso-position-vertical-relative"]),
492-
...(top === undefined ? {} : { posOffset: pixelsToEmu(top) }),
492+
posOffset: pixelsToEmu(top ?? 0),
493493
},
494494
};
495495
}

packages/core/src/docx/textBoxDrawingOwnership.test.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { describe, expect, test } from "bun:test";
22
import JSZip from "jszip";
33

4+
import type { ImagePosition } from "../types/document";
45
import { parseDocx } from "./parser";
56
import { createEmptyDocx, repackDocx } from "./rezip";
67

@@ -97,6 +98,26 @@ const bodyShapeWrapTypes = ({ package: { document } }: ParsedDocx): string[] =>
9798
return wrapTypes;
9899
};
99100

101+
const bodyShapePositions = ({ package: { document } }: ParsedDocx): ImagePosition[] => {
102+
const positions: ImagePosition[] = [];
103+
for (const block of document.content) {
104+
if (block.type !== "paragraph") {
105+
continue;
106+
}
107+
for (const content of block.content) {
108+
if (content.type !== "run") {
109+
continue;
110+
}
111+
for (const item of content.content) {
112+
if (item.type === "shape" && item.shape.position) {
113+
positions.push(item.shape.position);
114+
}
115+
}
116+
}
117+
}
118+
return positions;
119+
};
120+
100121
const savedDocumentXml = async (buffer: ArrayBuffer): Promise<string> => {
101122
const zip = await JSZip.loadAsync(buffer);
102123
return zip.file("word/document.xml")!.async("text");
@@ -164,4 +185,50 @@ describe("text-box drawing ownership", () => {
164185
const reopened = await parseDocx(saved, { preloadFonts: false });
165186
expect(bodyShapeWrapTypes(reopened)).toEqual(["inline"]);
166187
});
188+
189+
test("positioned VML text boxes normalize every omitted axis offset before DrawingML conversion", async () => {
190+
const cases = [
191+
{
192+
style: "top:10pt",
193+
expectedPosition: {
194+
horizontal: { relativeTo: "character", posOffset: 0 },
195+
vertical: { relativeTo: "paragraph", posOffset: 127_000 },
196+
},
197+
},
198+
{
199+
style: "left:20pt",
200+
expectedPosition: {
201+
horizontal: { relativeTo: "character", posOffset: 254_000 },
202+
vertical: { relativeTo: "paragraph", posOffset: 0 },
203+
},
204+
},
205+
{
206+
style: "",
207+
expectedPosition: {
208+
horizontal: { relativeTo: "character", posOffset: 0 },
209+
vertical: { relativeTo: "paragraph", posOffset: 0 },
210+
},
211+
},
212+
] as const satisfies readonly {
213+
style: string;
214+
expectedPosition: ImagePosition;
215+
}[];
216+
217+
for (const { style, expectedPosition } of cases) {
218+
const source = await buildDocx(`
219+
<w:pict>
220+
<v:shape style="position:absolute;${style};width:2in;height:1in">
221+
<v:textbox>
222+
<w:txbxContent><w:p><w:r><w:t>Legacy text</w:t></w:r></w:p></w:txbxContent>
223+
</v:textbox>
224+
</v:shape>
225+
</w:pict>`);
226+
const parsed = await parseDocx(source, { preloadFonts: false });
227+
expect(bodyShapePositions(parsed)).toEqual([expectedPosition]);
228+
229+
const saved = await repackDocx(parsed, { updateModifiedDate: false });
230+
const reopened = await parseDocx(saved, { preloadFonts: false });
231+
expect(bodyShapePositions(reopened)).toEqual([expectedPosition]);
232+
}
233+
});
167234
});

0 commit comments

Comments
 (0)