Skip to content

Images in shapes ​

slide.addImage() clips a picture to an outline and, separately, controls how the source image fills the picture's box.

ts
import TsPptx from 'pptx-ts'

const pptx = new TsPptx()
const slide = pptx.addSlide()
slide.addImage({ path: 'avatar.png', x: 1, y: 1, w: 2, h: 2, shape: 'ellipse', sizing: { type: 'cover' } })
await pptx.writeFile({ fileName: 'avatar.pptx' })

Options at a glance ​

OptionTypeDefaultEffect
pointsGeometryPoint[]noneClip to a freeform path measured in the picture's own box. Wins over shape and rounding.
shapeSHAPE_NAME'rect'Clip to a PowerPoint preset such as 'roundRect' or 'hexagon'. Wins over rounding.
roundingbooleanfalseClip to an ellipse.
rectRadiusnumber, inchesthe preset's own radiusCorner radius for 'roundRect' and the other rounded presets.
shapeAdjustShapeAdjustValue or an arraynoneThe preset's adjustment handles, each value a 0 to 1 fraction. A value past a handle's limit is written as given, and PowerPoint draws the shape at that limit.
sizing{ type, x?, y?, w?, h? }noneHow the source fills the box: cover, contain, crop or stretch.
crop{ l?, t?, r?, b? }nonePercent trimmed off each edge of the source. Wins over sizing.
lineShapeLinePropsnoneOutline drawn along the clip.
shadowShadowPropsnoneShadow under the picture.
transparencynumber, 0 to 1000Picture transparency in percent.
duotone, grayscale, biLevel, clrChangesee Recolor a picturenoneRecolor the picture.

Clip a picture to a shape ​

One clip applies, picked in this order:

ts
// A preset with rounded corners
slide.addImage({ path: 'avatar.png', x: 1, y: 1, w: 2, h: 2, shape: 'roundRect', rectRadius: 0.25 })

// An ellipse
slide.addImage({ path: 'avatar.png', x: 4, y: 1, w: 2, h: 2, rounding: true })

// A freeform triangle
slide.addImage({
  path: 'photo.png', x: 7, y: 1, w: 2, h: 2,
  points: [{ x: 1, y: 0 }, { x: 2, y: 2 }, { x: 0, y: 2 }, { close: true }],
})
slide(0, 0)(w, h)
  • points are inches inside the picture's own box, 0 to w across and 0 to h down. They are not slide positions.
  • Write points in inches. A percentage string resolves against the slide, not against the box.
  • A path takes the same nodes as a freeform shape: plain points (the first moves the pen, moveTo: true starts a new subpath, the rest draw lines), a curve of type cubic, quadratic or arc, and { close: true }.
  • A clip changes only the outline. How the source fills the box is set by sizing.

Fill the shape without distortion ​

With no sizing, a raster stretches to the box, so a photo whose ratio differs from the box distorts. Pair the clip with a sizing mode.

sizing.typeScales the sourceCropsDistortsDefault for
stretchEach axis to the box on its ownNoYes, when the ratios differA raster with no sizing
coverUntil it covers the whole boxYes, the overflow, evenly on both sidesNoNothing
containUntil it fits inside the boxNo, it leaves empty bandsNoA measurable SVG with no sizing
cropNot at all: the image stays at w by hYes, to the x, y, w, h windowNoNothing
covercontainstretch
ts
// A tall hexagon filled by a wide photo, cropped rather than squashed
slide.addImage({ path: 'photo.jpg', x: 1, y: 1, w: 2, h: 3, shape: 'hexagon', sizing: { type: 'cover' } })

// A 2in square window cut out of the photo as drawn at 4in by 3in
slide.addImage({ path: 'photo.jpg', x: 1, y: 1, w: 4, h: 3, sizing: { type: 'crop', x: 1, y: 0.5, w: 2, h: 2 } })
  • sizing.w and sizing.h default to the picture's w and h. Any other value becomes the drawn size of the picture.
  • cover and contain read the source's natural size from the image data: the PNG, JPEG, GIF, BMP or WebP header, or an SVG's width and height, else its viewBox. When that fails they use the box's own ratio and warn.
  • sizing: { type: 'crop' } measures its window in inches on the image as drawn at w by h, and the picture shrinks to the window.
  • The crop option trims the source by percentages, so it works on any format. With both crop and sizing set, crop applies and sizing is ignored with a warning.

Place an SVG at its own aspect ratio ​

An SVG states its own ratio, so a vector source is placed differently from a raster:

  1. crop is set: the percentage crop applies, as for a raster.
  2. sizing is set: that mode applies. cover and contain use the SVG's width and height, else its viewBox. With neither, they use the box's ratio and warn.
  3. No sizing, and the SVG has a viewBox or a width and height: the SVG is letterboxed inside the box, exactly as contain would place it. When the ratios already match, it simply fills the box.
  4. No sizing, and the SVG has no intrinsic size: it stretches to the box, without a warning.

A raster with no sizing always stretches.

ts
// A square icon in a 3in by 1in box sits centred at its true ratio
slide.addImage({ svg: iconMarkup, x: 1, y: 1, w: 3, h: 1 })

// Ask for stretch where the distortion is wanted
slide.addImage({ svg: bandMarkup, x: 0, y: 0, w: 13.33, h: 0.4, sizing: { type: 'stretch' } })

The SVG's ratio also fills in a missing dimension. Given only w or only h, the other side follows the ratio, so { svg, w: 4 } on a 2:1 viewBox is 4in by 2in. Given neither, the picture is 1in by 1in: SVG user units are not read as pixels.

Cut a half-disc with clipPath() ​

clipPath(shape, w, h) returns the points for a named silhouette, sized to a w by h box. It has one silhouette, half-disc: a rectangle whose one side is replaced by an arc that bulges toward the opposite edge. A cover-slide picture placeholder cuts the same outline.

ts
import TsPptx, { clipPath } from 'pptx-ts'

const pptx = new TsPptx()
const slide = pptx.addSlide()
const w = 5.22
const h = 7.5

slide.addImage({
  path: 'cover-photo.jpg', x: 0, y: 0, w, h,
  points: clipPath({ kind: 'half-disc', flat: 'right' }, w, h),
  sizing: { type: 'cover' },
})
FieldValuesDefaultEffect
kind'half-disc'requiredThe silhouette.
flat'left' or 'right'requiredThe edge the straight side sits on. The arc bulges toward the other edge.
preset'deep' or 'shallow''deep''deep': the arc takes about 32% of the width, symmetric top to bottom. 'shallow': about 13%, with its tip just below mid-height.
deepshallow
  • Pass the same w and h the picture is drawn at. The path is in that box's inches, so a picture of a different size gets the clip in the wrong place.
  • Both presets are traced with two cubic curves, so neither is an exact half-ellipse.

Draw the arc yourself ​

For a silhouette clipPath does not name, write the path by hand. This one is a right-flush half-disc whose curved edge is a single arc node:

ts
const fx = 0.3179 * w // x of the flat edge

slide.addImage({
  path: 'cover-photo.jpg', x: 0, y: 0, w, h,
  points: [
    { x: fx, y: 0 },
    { x: w, y: 0 },
    { x: w, y: h },
    { x: fx, y: h },
    // the curved left edge, from the bottom of the flat edge back to its top
    { curve: { type: 'arc', hR: h / 2, wR: fx, stAng: 90, swAng: 180 } },
    { close: true },
  ],
  sizing: { type: 'cover' },
})
  • An arc node takes the radii hR and wR and the angles stAng and swAng in degrees. It has no end point: the arc starts at the pen and ends where the sweep lands. An x or y on it is ignored with a warning.
  • Arc angles are not wrapped: swAng: 400 sweeps 400 degrees, not 40.

Recolor a picture ​

line, shadow and transparency work on a clipped picture as on any other, and line follows the clip. Four options recolor the image:

OptionTypeEffect
duotone{ shadow: Color, highlight: Color }Maps dark tones to shadow and light tones to highlight.
grayscalebooleanTurns every pixel gray.
biLevel{ threshold: number }Black and white: pixels at or above the 0 to 1 luminance threshold turn white, the rest black.
clrChange{ from: Color, to: Color }Repaints pixels of one color as another.

Colors take a hex value or a theme color. Set one recolor option per picture: when several are set, all of them are written, and Picture.recolor reads back only the first.

ts
slide.addImage({
  path: 'photo.jpg', x: 1, y: 1, w: 3, h: 2,
  shape: 'roundRect',
  sizing: { type: 'cover' },
  line: { color: '0088CC', width: 2 },
  duotone: { shadow: '250F6B', highlight: 'FFFFFF' },
})

Invalid input ​

"On write" means the check runs when the deck is generated, not when addImage is called.

ConditionWarns or throwsCode
clipPath gets a kind, flat or preset it does not knowthrowsInvalidOptionError clip/invalid-shape
An arc node carries x or ywarns, ignores themgeometry/arc-node-point-ignored
An arc's stAng or swAng is not a finite numberthrows on writeInvalidOptionError geometry/arc-angle-non-finite
rectRadius is not a finite numberwarns, keeps the preset's radiusgeometry/invalid-shape-adjust
A shapeAdjust entry lacks a string name or a finite valuewarns, skips the entrygeometry/invalid-shape-adjust
cover or contain cannot read the source's natural sizewarns, uses the box's ratioimage/unmeasurable-natural-size
Both crop and sizing are setwarns, ignores sizingimage/crop-and-sizing-conflict
A sizing: { type: 'crop' } window reaches past the imagethrows on writeInvalidOptionError image/crop-window-overflows
A crop edge is outside 0 to 100throws on writeInvalidOptionError image/crop-inset-out-of-range
crop l plus r, or t plus b, reaches 100throws on writeInvalidOptionError image/crop-insets-exceed-extent
biLevel.threshold is outside 0 to 1warns, clamps itimage/bilevel-threshold-out-of-range
biLevel.threshold is not a numberthrows on writeInvalidOptionError percent/non-finite

Limits ​

  • clipPath has one silhouette, half-disc.
  • A clipPath result fits one box size. Call it again for a different w or h.
  • cover and contain crop evenly around the center. There is no focal point.
  • An SVG given neither w nor h is placed at 1in by 1in.
  • rectRadius is a length in inches, resolved against the shorter side of the box.

Reading it back ​

Open the deck with Presentation from pptx-ts/read. A picture reports its clip, crop and recolor:

ts
import { readFile } from 'node:fs/promises'
import { Presentation } from 'pptx-ts/read'

const presentation = await Presentation.load(await readFile('avatar.pptx'))
for (const shape of presentation.slides[0]?.shapes ?? []) {
  if (shape.shapeType === 'picture') console.log(shape.presetGeometry, shape.crop, shape.recolor)
}
AccessorReturns
presetGeometryThe preset clip, such as 'ellipse'. 'rect' for an unclipped picture, null for a freeform clip.
cropThe source crop as fractions, { left, top, right, bottom }, or null when there is none.
recolorThe first effect on the image: duotone, clrChange, grayscale, biLevel, or alphaModFix for transparency. A picture with transparency reports alphaModFix ahead of its recolor.
setImage(bytes, { contentType, fit })Swaps the image. fit: 'cover', 'contain' or 'stretch' recomputes the crop for the new image.

The read model has no accessor for a picture's freeform clip path. See Read object model for the rest of the Picture API.

See also ​