Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions docs/examples/images.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,36 @@ await Docx.fromJsx(
data={Deno.readFile('test/spacekees.jpeg')}
width={cm(16)}
height={cm(16)}
title="Title"
alt="Description"
title='Title'
alt='Description'
/>
</Text>
</Paragraph>,
</Paragraph>
).toFile('images.docx');
```

## Borders

Pass a `border` prop to draw a line around the image. Every option is optional; `width` defaults to
0.75pt and `color` to black, so `border={{}}` already gives you a thin black line. The `color` is a
hexadecimal code without leading hash, and `type` is one of the DrawingML dash styles (`solid`,
`dot`, `dash`, `lgDash`, `dashDot`, `lgDashDot`, `lgDashDotDot`, `sysDash`, `sysDot`, `sysDashDot`,
`sysDashDotDot`).

```tsx
/** @jsx Docx.jsx */
import Docx, { cm, Image, Paragraph, pt, Text } from 'docxml';

await Docx.fromJsx(
<Paragraph>
<Text>
<Image
data={Deno.readFile('test/spacekees.jpeg')}
width={cm(16)}
height={cm(16)}
border={{ width: pt(3), color: 'ff0000', type: 'dash' }}
/>
</Text>
</Paragraph>
).toFile('images.docx');
```
29 changes: 24 additions & 5 deletions examples/images.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** @jsx Docx.jsx */
import Docx, { cm, Image, Paragraph, Section, Text } from '../mod.ts';
import Docx, { cm, Image, Paragraph, pt, Section, Text } from '../mod.ts';

await Docx.fromJsx(
<Section>
Expand All @@ -9,8 +9,8 @@ await Docx.fromJsx(
data={Deno.readFile('assets/spacekees.jpeg')}
width={cm(16)}
height={cm(16)}
title="Title"
alt="Description"
title='Title'
alt='Description'
/>
</Text>
</Paragraph>
Expand All @@ -29,8 +29,27 @@ await Docx.fromJsx(
}}
width={cm(16)}
height={cm(16)}
title="Title"
alt="Description"
title='Title'
alt='Description'
/>
</Text>
</Paragraph>
<Paragraph>
<Text>This image has a configurable border.</Text>
</Paragraph>
<Paragraph>
<Text>
<Image
data={Deno.readFile('assets/spacekees.jpeg')}
width={cm(16)}
height={cm(16)}
title='Title'
alt='Description'
border={{
width: pt(3),
color: 'ff0000',
type: 'dash',
}}
/>
</Text>
</Paragraph>
Expand Down
2 changes: 1 addition & 1 deletion lib/Docx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export class Docx<
}

if (relationships !== null) {
component.ensureRelationship(relationships);
await component.ensureRelationship(relationships);
}

await Promise.all(
Expand Down
4 changes: 3 additions & 1 deletion lib/classes/src/Component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,9 @@ export abstract class Component<
*
* this.#relationshipId = relationships.add(RelationshipType.hyperlink, this.props.url);
*/
public ensureRelationship(_relationships: RelationshipsXml) {
public ensureRelationship(
_relationships: RelationshipsXml
): void | Promise<void> {
// no-op
}

Expand Down
123 changes: 122 additions & 1 deletion lib/components/document/src/Image.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,54 @@ import {
*/
export type ImageChild = never;

/**
* The line width that word processors use when a border does not specify one.
*/
const DEFAULT_BORDER_WIDTH_EMU = 9525;

/**
* The line color used when a border does not specify one.
*/
const DEFAULT_BORDER_COLOR = '000000';

export type DataExtensions = {
svg?: Promise<string>;
};

/**
* The dash style of an image border, as defined by DrawingML's `ST_PresetLineDashVal`.
*/
export type ImageBorderType =
| 'solid'
| 'dot'
| 'dash'
| 'lgDash'
| 'dashDot'
| 'lgDashDot'
| 'lgDashDotDot'
| 'sysDash'
| 'sysDot'
| 'sysDashDot'
| 'sysDashDotDot';

/**
* A type describing the border drawn around an {@link Image}.
*/
export type ImageBorder = {
/**
* The thickness of the border line.
*/
width?: null | Length;
/**
* The color of the border line, as a hexadecimal code without leading hash (`"ff0000"`).
*/
color?: null | string;
/**
* The dash style of the border line.
*/
type?: null | ImageBorderType;
};

/**
* A type describing the props accepted by {@link Image}.
*/
Expand All @@ -44,6 +88,11 @@ export type ImageProps = {
alt?: null | string;
width: Length;
height: Length;
/**
* The border drawn around this image. Omitting this prop, or any of its options, means that
* the word processor default is used.
*/
border?: null | ImageBorder;
/**
* RelationshipId when the image is imported from an existing DOCX file.
* This is used to preserve the relationship when re-serializing the file,
Expand Down Expand Up @@ -219,6 +268,37 @@ export class Image extends Component<ImageProps, ImageChild> {
);
}

let borderNode: Node | null = null;
const { border } = this.props;
if (border) {
borderNode = create(
`
element ${QNS.a}ln {
attribute w { $borderWidth },
attribute cap { "flat" },
attribute cmpd { "sng" },
attribute algn { "ctr" },
element ${QNS.a}solidFill {
element ${QNS.a}srgbClr {
attribute val { $borderColor }
}
},
if (exists($borderType)) then element ${QNS.a}prstDash {
attribute val { $borderType }
} else ()
}
`,
{
borderWidth: Math.round(
border.width?.emu ?? DEFAULT_BORDER_WIDTH_EMU
),
// Without an explicit fill a word processor draws no line at all.
borderColor: border.color || DEFAULT_BORDER_COLOR,
borderType: border.type ?? null,
}
);
}

return create(
`
element ${QNS.w}drawing {
Expand All @@ -227,6 +307,12 @@ export class Image extends Component<ImageProps, ImageChild> {
attribute cx { $width },
attribute cy { $height }
},
element ${QNS.wp}effectExtent {
attribute l { $effectExtent },

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

effectExtent is ST_Coordinate type (which is an integer) but isn't rounded. Round it with Math.round(...) 

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed 🙂

attribute t { $effectExtent },
attribute r { $effectExtent },
attribute b { $effectExtent }
},
element ${QNS.wp}docPr {
attribute id { $identifier },
attribute name { $name },
Expand Down Expand Up @@ -275,7 +361,8 @@ export class Image extends Component<ImageProps, ImageChild> {
element ${QNS.a}prstGeom {
attribute prst { "rect" },
element ${QNS.a}avLst {}
}
},
$borderNode
}
}
}
Expand All @@ -290,7 +377,13 @@ export class Image extends Component<ImageProps, ImageChild> {
height: Math.round(this.props.height.emu),
name: this.props.title || '',
desc: this.props.alt || '',
// A border line is drawn on the edge of the image, so it needs room outside the
// extent or word processors will clip it.
effectExtent: border
? Math.round(border.width?.emu ?? DEFAULT_BORDER_WIDTH_EMU)
: 0,
extensionList,
borderNode,
}
);
}
Expand Down Expand Up @@ -361,6 +454,7 @@ export class Image extends Component<ImageProps, ImageChild> {
title,
width,
height,
border: extractBorderFromPicNode(picNode),
relationshipId: main.relationshipId,
});
image.#meta.location = main.location;
Expand All @@ -377,6 +471,33 @@ export class Image extends Component<ImageProps, ImageChild> {

registerComponent(Image);

function extractBorderFromPicNode(picNode: Node | null): ImageBorder | null {
if (picNode === null) {
return null;
}
const lineNode = evaluateXPathToFirstNode(
`./${QNS.pic}spPr/${QNS.a}ln`,
picNode
);
if (lineNode === null) {
return null;
}
const width = evaluateXPathToString(`./@w/string()`, lineNode);
const color = evaluateXPathToString(
`./${QNS.a}solidFill/${QNS.a}srgbClr/@val/string()`,
lineNode
);
const type = evaluateXPathToString(
`./${QNS.a}prstDash/@val/string()`,
lineNode
);
return {
width: width ? emu(Number(width)) : null,
color: color || null,
type: (type as ImageBorderType) || null,
};
}

type ExtractedBlipNodeData = {
main: {
data: Promise<Uint8Array>;
Expand Down
Loading