Skip to content

Commit 8a2c9a7

Browse files
committed
feat: add alt, description, name, and anchor:page to ImageOptions
1 parent 0e4d1f3 commit 8a2c9a7

5 files changed

Lines changed: 148 additions & 3 deletions

File tree

src/odt/content.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1195,11 +1195,18 @@ function buildImageFrame(
11951195
}
11961196

11971197
const frame = el("draw:frame")
1198-
.attr("draw:name", `Image${imageCounter}`)
1198+
.attr("draw:name", image.name ?? `Image${imageCounter}`)
11991199
.attr("text:anchor-type", image.anchor)
12001200
.attr("svg:width", image.width)
12011201
.attr("svg:height", image.height);
12021202

1203+
if (image.alt) {
1204+
frame.appendChild(el("svg:title").text(image.alt));
1205+
}
1206+
if (image.description) {
1207+
frame.appendChild(el("svg:desc").text(image.description));
1208+
}
1209+
12031210
const drawImage = el("draw:image")
12041211
.attr("xlink:href", imagePath)
12051212
.attr("xlink:type", "simple")

src/odt/document.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,9 @@ export class OdtDocument {
363363
height: options.height,
364364
mimeType: options.mimeType,
365365
anchor: options.anchor ?? "paragraph",
366+
alt: options.alt,
367+
description: options.description,
368+
name: options.name,
366369
};
367370
this.elements.push({
368371
type: "image",

src/odt/paragraph-builder.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,9 @@ export class ParagraphBuilder {
133133
height: options.height,
134134
mimeType: options.mimeType,
135135
anchor: options.anchor ?? "as-character",
136+
alt: options.alt,
137+
description: options.description,
138+
name: options.name,
136139
};
137140
this.runs.push({ text: "", image: imageData });
138141
return this;

src/odt/types.ts

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -253,8 +253,39 @@ export interface ImageOptions {
253253
* How the image is anchored in the document.
254254
* - `"as-character"` — inline with text (default for images inside paragraphs)
255255
* - `"paragraph"` — anchored to the paragraph (default for standalone `addImage()`)
256+
* - `"page"` — anchored to the page (positioned relative to the page)
256257
*/
257-
anchor?: "as-character" | "paragraph";
258+
anchor?: "as-character" | "paragraph" | "page";
259+
260+
/**
261+
* Accessible label for the image. Maps to `<svg:title>` inside the draw frame.
262+
* Used by screen readers and accessibility tools.
263+
*
264+
* @example
265+
* { alt: "Company logo" }
266+
* { alt: "LaTeX: \\frac{1}{2}" }
267+
*/
268+
alt?: string;
269+
270+
/**
271+
* Detailed description of the image content. Maps to `<svg:desc>` inside
272+
* the draw frame. Useful for preserving metadata such as LaTeX source,
273+
* formula text, or extended captions for round-trip editing.
274+
*
275+
* @example
276+
* { description: "$\\frac{1}{2}$" }
277+
*/
278+
description?: string;
279+
280+
/**
281+
* Override the auto-generated frame name. Maps to `draw:name` on the
282+
* draw frame. Useful when stable, predictable names are needed for
283+
* round-trip editing or LibreOffice extension integration.
284+
*
285+
* @example
286+
* { name: "formula-1" }
287+
*/
288+
name?: string;
258289
}
259290

260291
/**
@@ -274,7 +305,16 @@ export interface ImageData {
274305
mimeType: string;
275306

276307
/** Anchor type. */
277-
anchor: "as-character" | "paragraph";
308+
anchor: "as-character" | "paragraph" | "page";
309+
310+
/** Accessible label — maps to `<svg:title>`. */
311+
alt?: string;
312+
313+
/** Detailed description — maps to `<svg:desc>`. */
314+
description?: string;
315+
316+
/** Override for the draw:name attribute on the frame. */
317+
name?: string;
278318
}
279319

280320
// ─── List Types ──────────────────────────────────────────────────────

tests/odt-document.test.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1461,6 +1461,98 @@ describe("OdtDocument", () => {
14611461
expect(content).toContain('xmlns:draw="urn:oasis:names:tc:opendocument:xmlns:drawing:1.0"');
14621462
expect(content).toContain('xmlns:xlink="http://www.w3.org/1999/xlink"');
14631463
});
1464+
1465+
it("should emit svg:title when alt is provided", async () => {
1466+
const doc = new OdtDocument();
1467+
doc.addImage(TEST_PNG, {
1468+
width: "10cm",
1469+
height: "6cm",
1470+
mimeType: "image/png",
1471+
alt: "Company logo",
1472+
});
1473+
1474+
const content = await getContentXml(doc);
1475+
expect(content).toContain("<svg:title>Company logo</svg:title>");
1476+
expect(content).not.toContain("svg:desc");
1477+
});
1478+
1479+
it("should emit svg:desc when description is provided", async () => {
1480+
const doc = new OdtDocument();
1481+
doc.addImage(TEST_PNG, {
1482+
width: "10cm",
1483+
height: "6cm",
1484+
mimeType: "image/png",
1485+
description: "A photograph of the office building",
1486+
});
1487+
1488+
const content = await getContentXml(doc);
1489+
expect(content).toContain("<svg:desc>A photograph of the office building</svg:desc>");
1490+
expect(content).not.toContain("svg:title");
1491+
});
1492+
1493+
it("should emit both svg:title and svg:desc before draw:image when both are provided", async () => {
1494+
const doc = new OdtDocument();
1495+
doc.addImage(TEST_PNG, {
1496+
width: "2cm",
1497+
height: "0.5cm",
1498+
mimeType: "image/png",
1499+
alt: "LaTeX: \\frac{1}{2}",
1500+
description: "$\\frac{1}{2}$",
1501+
});
1502+
1503+
const content = await getContentXml(doc);
1504+
expect(content).toContain("<svg:title>LaTeX: \\frac{1}{2}</svg:title>");
1505+
expect(content).toContain("<svg:desc>$\\frac{1}{2}$</svg:desc>");
1506+
const titlePos = content.indexOf("<svg:title>");
1507+
const descPos = content.indexOf("<svg:desc>");
1508+
const imagePos = content.indexOf("draw:image");
1509+
expect(titlePos).toBeLessThan(descPos);
1510+
expect(descPos).toBeLessThan(imagePos);
1511+
});
1512+
1513+
it("should use caller-supplied name as draw:name", async () => {
1514+
const doc = new OdtDocument();
1515+
doc.addImage(TEST_PNG, {
1516+
width: "2cm",
1517+
height: "0.5cm",
1518+
mimeType: "image/png",
1519+
name: "formula-1",
1520+
});
1521+
1522+
const content = await getContentXml(doc);
1523+
expect(content).toContain('draw:name="formula-1"');
1524+
expect(content).not.toContain('draw:name="Image1"');
1525+
});
1526+
1527+
it("should support anchor type page", async () => {
1528+
const doc = new OdtDocument();
1529+
doc.addImage(TEST_PNG, {
1530+
width: "10cm",
1531+
height: "6cm",
1532+
mimeType: "image/png",
1533+
anchor: "page",
1534+
});
1535+
1536+
const content = await getContentXml(doc);
1537+
expect(content).toContain('text:anchor-type="page"');
1538+
});
1539+
1540+
it("should emit svg:title with alt in inline paragraph image", async () => {
1541+
const doc = new OdtDocument();
1542+
doc.addParagraph((p) => {
1543+
p.addImage(TEST_PNG, {
1544+
width: "2cm",
1545+
height: "0.5cm",
1546+
mimeType: "image/png",
1547+
alt: "LaTeX: \\frac{1}{2}",
1548+
description: "$\\frac{1}{2}$",
1549+
});
1550+
});
1551+
1552+
const content = await getContentXml(doc);
1553+
expect(content).toContain("<svg:title>LaTeX: \\frac{1}{2}</svg:title>");
1554+
expect(content).toContain("<svg:desc>$\\frac{1}{2}$</svg:desc>");
1555+
});
14641556
});
14651557

14661558
// ─── Repair Plan: Generation Side Fixes ─────────────────────────────

0 commit comments

Comments
 (0)