PPTX Studio
Packages

paint

Colour, fills, strokes and effects, each rule measured against PowerPoint rather than read from the standard.

@pptx-studio/paint0.4.1runs inTabWorkerNodeLive demo →npm →

Colours, gradients, pattern tiles, strokes and effects, each rule measured against PowerPoint rather than read from the standard.

npm install @pptx-studio/paint

What it is

How a colour transform chain resolves, how a gradient is really blended, what a pattern tile looks like, how a dash is compensated for its cap, how a shadow's blur radius becomes a Gaussian sigma: none of that is in ECMA-376 in a form you can implement from. @pptx-studio/paint holds those rules as functions, and each one cites the fixture it was measured against.

30-second example

colour.ts
// <a:schemeClr val="accent1"><a:lumMod val="60000"/><a:lumOff val="40000"/></a:schemeClr>
const color: Color = {
  space: 'scheme',
  name: 'accent1',
  transforms: [
    { op: 'lumMod', val: 60000 },
    { op: 'lumOff', val: 40000 },
  ],
};
toCss(resolveColor(color, { scheme, map })); // PowerPoint's own "Accent 1, Lighter 40%"

// Transforms apply in document order; reversed, they give a plausible wrong colour.
applyTransforms({ r: 68, g: 114, b: 196, a: 1 }, color.transforms);

Key concepts

Transforms apply in document order and clamp between steps

lumMod then lumOff is PowerPoint's "Lighter 40%"; the other order is a different, plausible, wrong colour. The 28 transforms are applied as written, clamping after each.

Six colour spaces, one resolver

srgb, scrgb, hsl, scheme, sys and prst all resolve through resolveColor; a scheme colour needs the theme's scheme and the sheet's colour map, which the model package supplies.

The two-colour gradient curve

PowerPoint does not blend two stops linearly. The measured curve is pre-sampled into a fifteen-knot ramp in a 2.2 power space, and every Office theme gradient has exactly that shape.

Pattern tiles are physical size

The 54 patterns are 8-by-8 pixel tiles at 96 DPI and do not scale with the shape. They were transcoded from Mono's libgdiplus.

Caps, joins, and the dash the cap eats

SVG adds a cap on top of a dash; PowerPoint paints the dash as ink. dashArray shortens each dash by a width and lengthens each gap by one so the ink matches.

API reference

173 exports of @pptx-studio/paint 0.4.1, read from the package this site installed. Click a name for its declaration.

src/errors.ts

  • PaintErrorCodetypeTyped failures, as everywhere else in this repository.
    type PaintErrorCode =
    /** An `a:srgbClr/@val` that is not six hex digits. */
    'COLOR_HEX' |
    /** A `ST_Percentage` or `ST_Angle` attribute that does not parse. */
    'COLOR_NUMBER' |
    /** A scheme colour name outside `ST_SchemeColorVal`. */
    'COLOR_SCHEME_NAME' |
    /** A `schemeClr` was resolved with no `clrScheme` in the context. */
    'COLOR_NO_SCHEME' |
    /** `schemeClr val="phClr"` outside a style invocation that supplies one. */
    'COLOR_NO_PHCLR' |
    /** A theme whose slots refer to each other in a cycle. */
    'COLOR_SCHEME_CYCLE' |
    /** A `sysClr` with no `lastClr` and a name this build has no value for. */
    'COLOR_SYS_UNKNOWN' |
    /** A `prstClr` name outside `ST_PresetColorVal`. */
    'COLOR_PRST_UNKNOWN' |
    /** A transform name outside the twenty-eight DrawingML defines. */
    'COLOR_TRANSFORM' |
    /**
     * An `a:gradFill` with no stops to interpolate between.
     *
     * PowerPoint refuses the package for this - both an empty `a:gsLst` and a
     * single stop come back repaired - so a deck in the wild will not contain one,
     * and a caller that has synthesised one has made a mistake worth naming.
     */
    'FILL_NO_STOPS' |
    /** A `pattFill/@prst` outside the 54 names PowerPoint accepts. */
    'FILL_PATTERN_UNKNOWN' |
    /**
     * A `prstDash/@val` outside the eleven `ST_PresetLineDashVal` names.
     *
     * PowerPoint refuses the package for this, so a deck in the wild will not
     * contain one and a caller that has synthesised one has made a mistake.
     */
    'LINE_DASH_UNKNOWN' |
    /** An `a:ln/@cmpd` outside the five `ST_CompoundLine` values. Also refused. */
    'LINE_COMPOUND_UNKNOWN' |
    /** A `headEnd`/`tailEnd` `@type` outside the six. Also refused. */
    'LINE_END_UNKNOWN' |
    /** A line end `@w` or `@len` that is not `sm`, `med` or `lg`. Also refused. */
    'LINE_END_SIZE' |
    /** A device scale that is not a positive number of pixels to the point. A caller mistake. */
    'LINE_DEVICE_SCALE' |
    /**
     * A negative blur radius.
     *
     * `a:outerShdw/@blurRad` and `a:glow/@rad` are both positive coordinates and
     * PowerPoint refuses a package carrying a negative one, so this is a caller
     * mistake rather than a file it will ever have to cope with.
     */
    'EFFECT_NEGATIVE_RADIUS' |
    /** An image fill whose resolution is unknown, so a tile has no natural size. */
    'FILL_BLIP_RESOLUTION' |
    /** An `a:srcRect` that crops the whole image away. */
    'FILL_BLIP_CROP' |
    /** A blip with no pixels in it. */
    'FILL_BLIP_SIZE' |
    /** An `a:tile` scaled to zero. */
    'FILL_BLIP_SCALE';
  • PaintErrorclass
    class PaintError extends Error {
      readonly code: PaintErrorCode;
      /** The offending token, when the failure is about one. */
      readonly token: string | null;
      constructor(code: PaintErrorCode, message: string, token?: string | null);
    }

src/colors/transfer.ts

  • ChanneltypeA channel, nominally 0..1 - but see clamp01 for why "nominally".
    type Channel = number;
  • toLinearfunctionsRGB to linear light. IEC 61966-2-1's piecewise curve, exactly as written.
    function toLinear(c: Channel): Channel;
  • fromLinearfunctionLinear light back to sRGB.
    function fromLinear(c: Channel): Channel;
  • clamp01function
    function clamp01(c: number): number;
  • HsltypeHue in degrees, saturation and lightness as fractions.
    interface Hsl {
      h: number;
      s: number;
      l: number;
    }
  • rgbToHslfunctionThe bi-hexcone conversion. Hue of a grey is 0, which is what PowerPoint uses.
    function rgbToHsl(r: Channel, g: Channel, b: Channel): Hsl;
  • hslToRgbfunctionBack to RGB. Out-of-range s and l are carried through the arithmetic rather than rejected, and the result may fall outside 0..1 - that is the point of not clamping in HSL, and the caller clamps.
    function hslToRgb(hsl: Hsl): [Channel, Channel, Channel];
  • wrapHuefunctionFold a hue in degrees into [0, 360).
    function wrapHue(h: number): number;
  • luma709functionRec.709 luma of the sRGB values, with no linearisation.
    function luma709(r: Channel, g: Channel, b: Channel): Channel;
  • toBytefunctionA channel to a byte, breaking an exact tie towards an odd result.
    function toByte(c: Channel): number;
  • parseHexfunctionSplit 4472C4 into three channels. Case-insensitive; six digits, no #.
    function parseHex(value: string): [Channel, Channel, Channel] | null;
  • toHexfunctionSix upper-case hex digits, the way the file format writes them.
    function toHex(r: Channel, g: Channel, b: Channel): string;

src/types.ts

  • PercentTransformOptypeTransforms whose val is a ST_Percentage family type: hundred-thousandths.
    type PercentTransformOp = 'tint' | 'shade' | 'alpha' | 'alphaOff' | 'alphaMod' | 'hueMod' | 'sat' | 'satOff' | 'satMod' | 'lum' | 'lumOff' | 'lumMod' | 'red' | 'redOff' | 'redMod' | 'green' | 'greenOff' | 'greenMod' | 'blue' | 'blueOff' | 'blueMod';
  • AngleTransformOptypeTransforms whose val is an angle: sixtieths of a degree.
    type AngleTransformOp = 'hue' | 'hueOff';
  • FlagTransformOptypeTransforms with no attribute at all.
    type FlagTransformOp = 'comp' | 'inv' | 'gray' | 'gamma' | 'invGamma';
  • ColorTransformOptype
    type ColorTransformOp = PercentTransformOp | AngleTransformOp | FlagTransformOp;
  • ColorTransformtypeOne transform, in document order.
    type ColorTransform = {
      readonly op: PercentTransformOp;
      readonly val: number;
    } | {
      readonly op: AngleTransformOp;
      readonly val: number;
    } | {
      readonly op: FlagTransformOp;
    };
  • COLOR_TRANSFORM_OPSconstThe 28 members of EG_ColorTransform, for a parser to test a tag against.
    const COLOR_TRANSFORM_OPS: readonly ColorTransformOp[];
  • transformShapefunctionWhich of the three shapes a transform tag takes. Drives parsing, not resolution.
    function transformShape(op: string): 'percent' | 'angle' | 'flag' | null;
  • SchemeSlottypeThe twelve children of a:clrScheme, and the only things a clrMap maps *to*.
    type SchemeSlot = 'dk1' | 'lt1' | 'dk2' | 'lt2' | 'accent1' | 'accent2' | 'accent3' | 'accent4' | 'accent5' | 'accent6' | 'hlink' | 'folHlink';
  • SCHEME_SLOTSconst
    const SCHEME_SLOTS: readonly SchemeSlot[];
  • MappedColorNametypeThe twelve attributes of p:clrMap, and the only names a map is consulted for.
    type MappedColorName = 'bg1' | 'tx1' | 'bg2' | 'tx2' | 'accent1' | 'accent2' | 'accent3' | 'accent4' | 'accent5' | 'accent6' | 'hlink' | 'folHlink';
  • MAPPED_COLOR_NAMESconst
    const MAPPED_COLOR_NAMES: readonly MappedColorName[];
  • SchemeColorNametypeEvery value a:schemeClr/@val may take: seventeen, not twelve and not sixteen.
    type SchemeColorName = SchemeSlot | MappedColorName | 'phClr';
  • ClrSchemetypeA parsed a:clrScheme. Each slot holds a colour, not a hex string - see below.
    type ClrScheme = Readonly<Record<SchemeSlot, Color>>;
  • ClrMaptypeA parsed p:clrMap or p:clrMapOvr. All twelve attributes are required.
    type ClrMap = Readonly<Record<MappedColorName, SchemeSlot>>;
  • ColortypeA colour exactly as the file wrote it: one of six bases, plus its transforms.
    type Color =
    /** `a:srgbClr` - six hex digits. */
    {
      readonly space: 'srgb';
      readonly hex: string;
      readonly transforms: readonly ColorTransform[];
    } |
    /** `a:scrgbClr` - three percentages, hundred-thousandths. */
    {
      readonly space: 'scrgb';
      readonly r: number;
      readonly g: number;
      readonly b: number;
      readonly transforms: readonly ColorTransform[];
    } |
    /** `a:hslClr` - hue in sixtieths of a degree, sat and lum in hundred-thousandths. */
    {
      readonly space: 'hsl';
      readonly hue: number;
      readonly sat: number;
      readonly lum: number;
      readonly transforms: readonly ColorTransform[];
    } |
    /** `a:schemeClr` - a name resolved against a theme and a colour map. */
    {
      readonly space: 'scheme';
      readonly name: SchemeColorName;
      readonly transforms: readonly ColorTransform[];
    } |
    /** `a:sysClr` - a machine colour, with the last value the writer saw. */
    {
      readonly space: 'sys';
      readonly name: string;
      readonly lastClr: string | null;
      readonly transforms: readonly ColorTransform[];
    } |
    /** `a:prstClr` - one of the 140 names in `ST_PresetColorVal`. */
    {
      readonly space: 'prst';
      readonly name: string;
      readonly transforms: readonly ColorTransform[];
    };
  • RgbatypeA resolved colour. Channels are 0..1 and clamped; a is opacity, 1 is opaque.
    interface Rgba {
      readonly r: number;
      readonly g: number;
      readonly b: number;
      readonly a: number;
    }

src/apply.ts

  • applyTransformsfunctionApply one transform chain to a colour that has already been reduced to sRGB.
    function applyTransforms(base: Rgba, transforms: readonly ColorTransform[]): Rgba;

src/colors/resolve.ts

  • ColorContexttypeFrom a Color to a number, and the four things that have to be supplied before that is possible.
    interface ColorContext {
      /** The theme's `a:clrScheme`. Required by any `a:schemeClr`. */
      readonly scheme?: ClrScheme;
      /**
       * The `p:clrMap` in force, from the master or overridden by a layout or slide.
       * Required by a `schemeClr` naming one of the twelve mapped names.
       */
      readonly map?: ClrMap;
      /**
       * The colour the enclosing `a:fillRef`/`a:lnRef`/`a:effectRef` was invoked
       * with, which is what `schemeClr val="phClr"` means. Supplied by the style
       * resolver in 2.9; there is nothing sensible to default it to.
       */
      readonly phClr?: Rgba;
      /**
       * Values for `a:sysClr` when the element has no `@lastClr`. Defaults to the
       * table in `sys-colors.ts`, which is one Windows machine's light theme - see
       * the long note there about why `@lastClr` wins here and does not in
       * PowerPoint.
       */
      readonly systemColors?: Readonly<Record<string, string>>;
    }
  • mapSchemeNamefunctionWhich theme slot a a:schemeClr/@val names.
    function mapSchemeName(name: SchemeColorName, map: ClrMap | undefined): SchemeSlot | null;
  • resolveColorfunctionResolve a colour to numbers: the base, then its transforms in document order.
    function resolveColor(color: Color, ctx?: ColorContext): Rgba;
  • toHexColorfunctionSix upper-case hex digits. Alpha is dropped, because the format has nowhere to put it.
    function toHexColor(color: Rgba): string;
  • toCssfunctionA CSS colour.
    function toCss(color: Rgba): string;

src/parse.ts

  • parsePercentagefunctionA ST_Percentage attribute, in hundred-thousandths.
    function parsePercentage(token: string): number;
  • parseAnglefunctionAn angle attribute, in sixtieths of a degree.
    function parseAngle(token: string): number;
  • parseSrgbValuefunctiona:srgbClr/@val, normalised to six upper-case hex digits.
    function parseSrgbValue(token: string): string;

src/colors/preset-colors.ts

  • PRESET_COLORSconstST_PresetColorVal: 147 names, and the values PowerPoint paints for them.
    const PRESET_COLORS: Readonly<Record<string, string>>;
  • isPresetColorNamefunctionWhether a token is one of the 147.
    function isPresetColorName(name: string): boolean;

src/colors/sys-colors.ts

  • SYSTEM_COLORSconstST_SystemColorVal, and what Windows 11's default light theme gives for each.
    const SYSTEM_COLORS: Readonly<Record<string, string>>;
  • isSystemColorNamefunctionWhether a token is one of the thirty.
    function isSystemColorName(name: string): boolean;

src/fills/fill.ts

  • RelativeRecttypeCT_RelativeRect: four insets, in hundred-thousandths, each defaulting to zero.
    interface RelativeRect {
      readonly l: number;
      readonly t: number;
      readonly r: number;
      readonly b: number;
    }
  • WHOLE_RECTconst
    const WHOLE_RECT: RelativeRect;
  • TileFlipModetypeST_TileFlipMode.
    type TileFlipMode = 'none' | 'x' | 'y' | 'xy';
  • GradientStoptype
    interface GradientStop {
      /** `ST_PositiveFixedPercentage`, hundred-thousandths, as written. */
      readonly pos: number;
      readonly color: Color;
    }
  • LinearShadetypea:lin - a straight ramp at an angle.
    interface LinearShade {
      readonly kind: 'linear';
      /**
       * Sixtieths of a degree. Zero points along +x so stop 0 is on the left, and
       * the angle increases **clockwise on screen**, y pointing down: `5400000` runs
       * top to bottom. Measured on six angles against a square shape, each landing
       * within a tenth of a degree of the value written.
       */
      readonly ang: number;
      /**
       * `@scaled`. False means the written angle is the visual angle. True means it
       * is measured in a space where the shape is a unit square - see
       * `linearGradientVector`, which is where the aspect correction actually
       * happens and where the direction it bends is recorded.
       */
      readonly scaled: boolean;
    }
  • PathShadetypea:path - a ramp that spreads out from a focus.
    interface PathShade {
      readonly kind: 'path';
      readonly path: 'shape' | 'circle' | 'rect';
      /**
       * `a:fillToRect`, or `null` when the element was absent - which is a different
       * focus from four zero insets, measured. ADR 0022.
       */
      readonly fillToRect: RelativeRect | null;
    }
  • GradientShadetype
    type GradientShade = LinearShade | PathShade;
  • GradientFilltype
    interface GradientFill {
      readonly type: 'gradient';
      /** As written, in document order. `resolveGradientStops` is what sorts them. */
      readonly stops: readonly GradientStop[];
      /** `null` when the markup carried neither `a:lin` nor `a:path`. */
      readonly shade: GradientShade | null;
      /** `a:tileRect`, or `null` for the default. Insets, and they may be negative. */
      readonly tileRect: RelativeRect | null;
      /**
       * `@flip`. Recorded so it round-trips, and otherwise unused: measured to have
       * no effect on what PowerPoint paints. See `gradient.ts`.
       */
      readonly flip: TileFlipMode;
      /** `@rotWithShape`. Defaults to true, and PowerPoint writes it explicitly. */
      readonly rotWithShape: boolean;
    }
  • PatternFilltype
    interface PatternFill {
      readonly type: 'pattern';
      /** `ST_PresetPatternVal`. One of the 54 in `pattern-tiles.ts`. */
      readonly prst: string;
      /** The marks. `null` when the element omitted `a:fgClr`. */
      readonly fg: Color | null;
      /** The ground. `null` when the element omitted `a:bgClr`. */
      readonly bg: Color | null;
    }
  • SolidFilltype
    interface SolidFill {
      readonly type: 'solid';
      readonly color: Color;
    }
  • NoFilltype
    interface NoFill {
      readonly type: 'none';
    }
  • GroupFilltypea:grpFill - not noFill.
    interface GroupFill {
      readonly type: 'group';
    }
  • TileAligntypeWhere a:tile puts one whole tile against the shape box. ADR 0036.
    type TileAlign = 'tl' | 't' | 'tr' | 'l' | 'ctr' | 'r' | 'bl' | 'b' | 'br';
  • BlipEffecttypeOne a:blip colour effect, in document order.
    type BlipEffect = {
      readonly kind: 'grayscale';
    } | {
      readonly kind: 'biLevel';
      readonly thresh: number;
    } | {
      readonly kind: 'lum';
      readonly bright: number;
      readonly contrast: number;
    } | {
      readonly kind: 'duotone';
      readonly from: Color;
      readonly to: Color;
    } | {
      readonly kind: 'alphaModFix';
      readonly amt: number;
    } | {
      readonly kind: 'clrChange';
      readonly from: Color;
      readonly to: Color;
      readonly useAlpha: boolean;
    };
  • BlipStretchtypea:stretch - one copy of the source, mapped onto the shape box.
    interface BlipStretch {
      readonly kind: 'stretch';
      /** `a:fillRect`: insets of the destination, and they may be negative. */
      readonly fillRect: RelativeRect;
    }
  • BlipTiletypea:tile - the source repeated on a lattice.
    interface BlipTile {
      readonly kind: 'tile';
      /** `@tx`/`@ty` in EMU, positive right and down, from the `@algn` anchor. */
      readonly tx: number;
      readonly ty: number;
      /** `@sx`/`@sy` in hundred-thousandths of the natural tile size. */
      readonly sx: number;
      readonly sy: number;
      /** `@flip`. Mirrors ALTERNATE tiles, so the visible period doubles. */
      readonly flip: TileFlipMode;
      readonly algn: TileAlign;
    }
  • BlipFilltypea:blipFill - an image fill.
    interface BlipFill {
      readonly type: 'blip';
      /**
       * `a:blip/@r:embed`, which the caller resolves against the part's own rels -
       * the same contract `buBlip` uses.
       *
       * `null` when the blip names no raster at all, which is what PowerPoint
       * writes for a picture that is only an SVG. ADR 0037.
       */
      readonly embed: string | null;
      /** `asvg:svgBlip/@r:embed`: the vector original, drawn from 10.7. */
      readonly svgEmbed: string | null;
      /** The part whose rels `embed` is scoped to; rIds are per-part, never global. */
      readonly part: string;
      /** `a:srcRect`: insets of the SOURCE, as fractions of the image. */
      readonly srcRect: RelativeRect;
      readonly mode: BlipStretch | BlipTile;
      readonly effects: readonly BlipEffect[];
      /** `@dpi`. Zero, which is what PowerPoint writes, means the image's own. */
      readonly dpi: number;
      readonly rotWithShape: boolean;
    }
  • Filltype
    type Fill = NoFill | SolidFill | GradientFill | PatternFill | GroupFill | BlipFill;

src/fills/gradient-ramp.ts

  • RAMP_BLEND_GAMMAconstThe space the two-stop curve blends in: a plain 2.2 power, measured.
    const RAMP_BLEND_GAMMA = 2.2;
  • TWO_STOP_RAMPconst[position, weight] pairs, position and weight both 0 to 1.
    const TWO_STOP_RAMP: readonly (readonly [number, number])[];
  • toRampSpacefunctionInto the blend space.
    function toRampSpace(channel: number): number;
  • fromRampSpacefunctionOut of the blend space.
    function fromRampSpace(value: number): number;
  • twoStopWeightfunctionThe blend weight at t.
    function twoStopWeight(t: number): number;

src/fills/gradient.ts

  • sortStopsfunctionThe stops in the order PowerPoint paints them: sorted by @pos, stably.
    function sortStops(stops: readonly GradientStop[]): readonly GradientStop[];
  • ResolvedStoptypeA stop with its colour resolved, position as a fraction.
    interface ResolvedStop {
      /** 0 to 1. */
      readonly pos: number;
      readonly color: Rgba;
    }
  • TwoColorRamptypeA gradient PowerPoint draws with its gamma-corrected two-colour path.
    interface TwoColorRamp {
      /** The colour at factor 0 - whatever sits at position 0. */
      readonly from: Rgba;
      /** The other colour. */
      readonly to: Rgba;
      /** One factor per sorted stop: 0 where the colour is `from`, 1 where it is `to`. */
      readonly factors: readonly number[];
    }
  • twoColorRampfunctionIs this a gradient PowerPoint draws with a curve rather than linearly?
    function twoColorRamp(stops: readonly ResolvedStop[]): TwoColorRamp | null;
  • resolveGradientStopsfunctionThe stops a renderer should emit, resolved and in paint order.
    function resolveGradientStops(fill: GradientFill, ctx?: ColorContext): readonly ResolvedStop[];
  • gradientColorAtfunctionThe colour at t along a gradient - what a viewer will actually paint.
    function gradientColorAt(fill: GradientFill, t: number, ctx?: ColorContext): Rgba;
  • GradientVectortype
    interface GradientVector {
      readonly x1: number;
      readonly y1: number;
      readonly x2: number;
      readonly y2: number;
    }
  • linearGradientVectorfunctionWhere a linear gradient's ramp starts and ends, in shape coordinates.
    function linearGradientVector(shade: LinearShade, w: number, h: number): GradientVector;
  • insetRectfunctionTurn a CT_RelativeRect's insets into a rectangle in 0..1 shape fractions.
    function insetRect(rect: RelativeRect): {
      l: number;
      t: number;
      r: number;
      b: number;
    };
  • PathGeometrytype
    interface PathGeometry {
      /** The focus, in shape units from the shape's top-left corner. */
      readonly fx: number;
      readonly fy: number;
      /**
       * How the distance from the focus is measured.
       *
       * `radial` is a true circle - isotropic in shape units, so on a wide shape the
       * bands are circles and not ellipses. `box` is the Chebyshev distance, giving
       * concentric rectangles. `outline` follows the shape's own geometry, which for
       * a rectangle is `box` and for an ellipse is the ellipse.
       */
      readonly metric: 'radial' | 'box' | 'outline';
      /** For `radial`: the circle the last stop lands on, always the shape's own. */
      readonly cx: number;
      readonly cy: number;
      readonly radius: number;
      /** The shape, in the same units, which is what `box` normalises against. */
      readonly w: number;
      readonly h: number;
    }
  • pathGeometryfunctionWhere a path gradient's ramp starts and how far it reaches.
    function pathGeometry(shade: PathShade, w: number, h: number): PathGeometry;
  • pathPositionAtfunctionHow far along the ramp the point (x, y) is, for a path gradient.
    function pathPositionAt(geometry: PathGeometry, x: number, y: number): number;
  • SvgStoptypeOne <stop>'s attributes, ready for either renderer.
    interface SvgStop {
      readonly offset: number;
      readonly color: string;
      readonly opacity: number;
    }
  • svgStopsfunctionThe stop list an SVG <linearGradient> or <radialGradient> needs.
    function svgStops(fill: GradientFill, ctx?: ColorContext): readonly SvgStop[];

src/fills/blip.ts

  • RecttypeA rectangle in whatever units its producer named.
    interface Rect {
      readonly x: number;
      readonly y: number;
      readonly w: number;
      readonly h: number;
    }
  • ImageSizetypeWhat the caller knows about the decoded image.
    interface ImageSize {
      readonly widthPx: number;
      readonly heightPx: number;
      /** The image's own declared resolution, which is what sizes a tile. */
      readonly dpi: number;
    }
  • StretchPlacementtypeOne copy of the source stretched onto a destination.
    interface StretchPlacement {
      readonly kind: 'stretch';
      /** Where the whole image goes, in the shape's own EMU, before cropping. */
      readonly image: Rect;
      /** The part of that destination the crop leaves visible. */
      readonly clip: Rect;
    }
  • TilePlacementtypeThe lattice, and the cell that repeats on it.
    interface TilePlacement {
      readonly kind: 'tile';
      /** One tile, in the shape's own EMU. */
      readonly tileW: number;
      readonly tileH: number;
      /** The top-left corner of the tile the `@algn` anchor placed. */
      readonly originX: number;
      readonly originY: number;
      readonly flipX: boolean;
      readonly flipY: boolean;
      /** Where the whole image sits inside one tile, once the crop is undone. */
      readonly image: Rect;
    }
  • BlipPlacementtype
    type BlipPlacement = StretchPlacement | TilePlacement;
  • BLIP_LUMAconstBT.709, which is what a:grayscl, a:biLevel and a:duotone all weigh by.
    const BLIP_LUMA: {
      readonly r: 0.2126;
      readonly g: 0.7152;
      readonly b: 0.0722;
    };
  • contrastScalefunctionThe slope a:lum/@contrast applies about mid-grey.
    function contrastScale(contrast: number): number;
  • brightOffsetfunctionWhat a:lum/@bright adds, in 0..1, once contrast has scaled the channel.
    function brightOffset(bright: number, scale: number): number;
  • blipPlacementfunctionWhere an image fill draws, in the shape box's own coordinates.
    function blipPlacement(fill: BlipFill, box: Rect, image: ImageSize): BlipPlacement;

src/fills/pattern-tiles.ts

  • PatternTiletypeOne tile, at its minimal repeating period.
    interface PatternTile {
      /** Width of the repeat, in pixels at 96 DPI. */
      readonly w: number;
      /** Height of the repeat, in pixels at 96 DPI. */
      readonly h: number;
      /**
       * Row-major coverage, two hex digits per pixel: `00` is the background
       * colour, `FF` the foreground, and anything between is antialiasing.
       */
      readonly cover: string;
    }
  • PATTERN_PIXEL_EMUconstOne tile pixel, in EMU. 914400 / 96.
    const PATTERN_PIXEL_EMU = 9525;
  • PATTERN_TILE_PIXELSconstThe full tile is always eight of those square, whatever the minimal period is.
    const PATTERN_TILE_PIXELS = 8;
  • PATTERN_TILESconst
    const PATTERN_TILES: Readonly<Record<string, PatternTile>>;
  • PRESET_PATTERN_NAMESconstThe 54 names, in MsoPatternType order - the order PowerPoint enumerated them.
    const PRESET_PATTERN_NAMES: readonly string[];
  • ANTIALIASED_PATTERNSconstThe three tiles PowerPoint antialiases rather than drawing one-bit.
    const ANTIALIASED_PATTERNS: readonly string[];
  • isPresetPatternNamefunctionIs this one of the 54?
    function isPresetPatternName(name: string): boolean;

src/fills/pattern.ts

  • ResolvedPatterntype
    interface ResolvedPattern {
      /** Width of the repeat, in EMU. */
      readonly width: number;
      /** Height of the repeat, in EMU. */
      readonly height: number;
      /** Coverage per pixel, row-major, 0 to 1. `length` is `cols * rows`. */
      readonly coverage: readonly number[];
      readonly cols: number;
      readonly rows: number;
      readonly fg: Rgba;
      readonly bg: Rgba;
    }
  • resolvePatternfunctionA pattern fill, ready to draw.
    function resolvePattern(fill: PatternFill, ctx?: ColorContext): ResolvedPattern;
  • patternPixelfunctionThe colour of one tile pixel.
    function patternPixel(pattern: ResolvedPattern, col: number, row: number): Rgba;
  • patternInkfunctionWhat fraction of the tile is foreground.
    function patternInk(name: string): number;

src/lines/compound-table.ts

  • COMPOUND_RUNS_60constAlternating rail and gap widths, in sixtieths of a:ln/@w, outside first.
    const COMPOUND_RUNS_60: Readonly<Record<string, readonly number[]>>;
  • COMPOUND_RUNSconstThe same, as fractions - convenient to read, and not what the maths uses.
    const COMPOUND_RUNS: Readonly<Record<string, readonly number[]>>;
  • COMPOUND_NAMESconst
    const COMPOUND_NAMES: readonly string[];
  • isCompoundNamefunctionDeliberately not a type predicate; see isPresetDashName for why.
    function isCompoundName(name: string): boolean;

src/lines/dash-table.ts

  • DashSegmenttypeOne dash and the gap that follows it, both in multiples of the stroke width.
    interface DashSegment {
      readonly d: number;
      readonly sp: number;
    }
  • PRESET_DASHESconst
    const PRESET_DASHES: Readonly<Record<string, readonly DashSegment[]>>;
  • PRESET_DASH_NAMESconstThe eleven ST_PresetLineDashVal names, as PowerPoint spelled them itself.
    const PRESET_DASH_NAMES: readonly string[];
  • isPresetDashNamefunctionDeliberately not a type predicate.
    function isPresetDashName(name: string): boolean;

src/lines/marker-table.ts

  • MarkerAnchortypeWhich end of the marker sits on the line's own endpoint.
    type MarkerAnchor = 'tip' | 'centre';
  • Markertype
    interface Marker {
      /** The family the silhouette was fitted to. */
      readonly outline: string;
      readonly anchor: MarkerAnchor;
      /** False for `arrow`, which is stroked rather than filled. */
      readonly filled: boolean;
      /** How far the back is cut forward, as a fraction of the length. Zero for most. */
      readonly notch: number;
      /**
       * The outline in a unit box: x from 0 at the back to 1 at the tip, y from
       * -0.5 to 0.5. Scale by (len x w, wid x w) to get the marker at a stroke
       * width of w.
       */
      readonly path: string;
    }
  • MARKER_SIZEconst@len and @w, in multiples of the stroke width.
    const MARKER_SIZE: Readonly<Record<string, number>>;
  • MARKER_SIZE_NAMESconst
    const MARKER_SIZE_NAMES: readonly string[];
  • MARKERSconst
    const MARKERS: Readonly<Record<string, Marker>>;
  • MARKER_TYPE_NAMESconst
    const MARKER_TYPE_NAMES: readonly string[];
  • isMarkerTypeNamefunctionDeliberately not a type predicate; see isPresetDashName for why.
    function isMarkerTypeName(name: string): boolean;
  • isMarkerSizeNamefunctionDeliberately not a type predicate.
    function isMarkerSizeName(name: string): boolean;

src/lines/line.ts

  • EMU_PER_POINTconstEnglish Metric Units in one point. A stroke width is always EMU.
    const EMU_PER_POINT = 12700;
  • DEFAULT_LINE_WIDTHconstThe width of an a:ln that does not say.
    const DEFAULT_LINE_WIDTH = 9525;
  • DEFAULT_LINE_CAPconstThe cap of an a:ln that does not say.
    const DEFAULT_LINE_CAP: LineCap;
  • DEFAULT_LINE_JOINconstThe join of an a:ln with no join element.
    const DEFAULT_LINE_JOIN: LineJoinKind;
  • DEFAULT_MITER_LIMITconstThe miter limit of an a:miter with no @lim, as a multiple of the width.
    const DEFAULT_MITER_LIMIT = 8;
  • LineCaptype
    type LineCap = 'flat' | 'sq' | 'rnd';
  • LineJoinKindtype
    type LineJoinKind = 'round' | 'bevel' | 'miter';
  • PenAlignmenttype
    type PenAlignment = 'ctr' | 'in';
  • LineEndSizetype
    type LineEndSize = 'sm' | 'med' | 'lg';
  • LineJointype
    interface LineJoin {
      readonly kind: LineJoinKind;
      /** A multiple of the stroke width. `null` means the attribute was absent. */
      readonly limit: number | null;
    }
  • LineEndtypea:headEnd or a:tailEnd.
    interface LineEnd {
      readonly type: string;
      readonly w: LineEndSize;
      readonly len: LineEndSize;
    }
  • LineDashtype
    type LineDash = {
      readonly kind: 'preset';
      readonly val: string;
    } | {
      readonly kind: 'custom';
      readonly stops: readonly DashSegment[];
    };
  • LinetypeCT_LineProperties, lazily.
    interface Line {
      readonly w: number | null;
      readonly cap: LineCap | null;
      readonly cmpd: string | null;
      readonly algn: PenAlignment | null;
      /** `null` is a genuine absence; an explicit `a:noFill` is `{ type: 'none' }`. */
      readonly fill: Fill | null;
      readonly dash: LineDash | null;
      readonly join: LineJoin | null;
      readonly headEnd: LineEnd | null;
      readonly tailEnd: LineEnd | null;
    }
  • ResolvedLinetype
    interface ResolvedLine {
      /** EMU. */
      readonly width: number;
      readonly cap: LineCap;
      readonly join: LineJoin & {
        readonly limit: number;
      };
      readonly algn: PenAlignment;
      readonly cmpd: string;
      readonly fill: Fill;
      readonly dash: readonly DashSegment[] | null;
      readonly headEnd: LineEnd | null;
      readonly tailEnd: LineEnd | null;
    }
  • resolveLinefunctionEvery property the file left unstated, filled in with what Office does.
    function resolveLine(line: Line): ResolvedLine;
  • dashSegmentsfunctionThe on/off pattern for a dash, in multiples of the stroke width.
    function dashSegments(dash: LineDash): readonly DashSegment[];
  • dashArrayfunctionThe dash pattern in EMU, ready for SVG's stroke-dasharray.
    function dashArray(segments: readonly DashSegment[], width: number, cap: LineCap): number[];
  • StrokeRailtypeOne rail of a compound stroke, as offsets from the geometry in EMU.
    interface StrokeRail {
      /** Distance from the path to the rail's centre. Negative is outward. */
      readonly offset: number;
      /** EMU. */
      readonly width: number;
    }
  • compoundRailsfunctionA compound stroke's rails, measured out from the geometry.
    function compoundRails(cmpd: string, width: number, algn: PenAlignment): StrokeRail[];
  • MarkerGeometrytypeThe marker for one line end, in EMU.
    interface MarkerGeometry {
      readonly type: string;
      /** EMU, along the line. */
      readonly length: number;
      /** EMU, across it. */
      readonly width: number;
      /** EMU from the marker's back edge to the line's endpoint. */
      readonly refX: number;
      readonly filled: boolean;
      /** The outline in a unit box; scale by `length` and `width`. */
      readonly path: string;
    }
  • markerGeometryfunction
    function markerGeometry(end: LineEnd | null, strokeWidth: number): MarkerGeometry | null;
  • markerOvershootfunctionWhat a marker adds to the shape's ink extent, in EMU beyond the endpoint.
    function markerOvershoot(end: LineEnd | null, strokeWidth: number): number;
  • MIN_PX_PER_PTconstThe smallest device scale a stroke is rounded for: a twentieth of a pixel to the point, 5 % zoom.
    const MIN_PX_PER_PT = 0.05;
  • DevicePentypeA stroke on the export's device grid: its pen, and where the pen sits (F2 and F3).
    interface DevicePen {
      /** The pen, in EMU: the width in whole device pixels, a half up, never under one. */
      readonly width: number;
      /** Half a device pixel in EMU when the pen is odd, else zero: an odd pen sits on pixel centres. */
      readonly shift: number;
    }
  • devicePenfunctionThe pen a stroke is drawn with at pxPerPt device pixels to the point.
    function devicePen(widthEmu: number, pxPerPt: number): DevicePen;
  • deviceStrokeWidthfunctionThe width a stroke is drawn at, in EMU, at pxPerPt device pixels to the point.
    function deviceStrokeWidth(widthEmu: number, pxPerPt: number): number;
  • MIN_MARKER_PENconstThe narrowest pen a line end is sized from: two points.
    const MIN_MARKER_PEN: number;
  • markerPenfunctionThe width a line end is sized from, in EMU: two points at or under two, else the drawn stroke, and never under a device pixel at pxPerPt (F2's M8, zoom.json).
    function markerPen(widthEmu: number, pxPerPt: number | null): number;
  • SvgStroketypeSVG stroke attributes for a single-rail stroke, in EMU.
    interface SvgStroke {
      readonly 'stroke-width': number;
      readonly 'stroke-linecap': 'butt' | 'square' | 'round';
      readonly 'stroke-linejoin': 'round' | 'bevel' | 'miter';
      readonly 'stroke-miterlimit': number;
      readonly 'stroke-dasharray': readonly number[] | null;
    }
  • svgStrokefunctionThe stroke attributes for one rail.
    function svgStroke(line: ResolvedLine, railWidth?: number): SvgStroke;

src/effect.ts

  • BLUR_RADIUS_TO_SIGMAconstThe Gaussian standard deviation for a DrawingML blur radius, in the same unit.
    const BLUR_RADIUS_TO_SIGMA: number;
  • EDGE_GROWTH_PER_RADIUSconstHow far a glow grows the shape, and a soft edge shrinks it, per unit of r.
    const EDGE_GROWTH_PER_RADIUS = 0.9;
  • GLOW_RADIUS_SCALEconstA glow uses half the radius it declares.
    const GLOW_RADIUS_SCALE = 0.5;
  • RectAlignmenttype
    type RectAlignment = 'tl' | 't' | 'tr' | 'l' | 'ctr' | 'r' | 'bl' | 'b' | 'br';
  • DEFAULT_SHADOW_ALIGNconstThe anchor a shadow's scale and skew are applied about, when @algn is absent.
    const DEFAULT_SHADOW_ALIGN: RectAlignment;
  • ShadowGeometrytype
    interface ShadowGeometry {
      /** EMU. */
      readonly blurRad: number;
      /** EMU. */
      readonly dist: number;
      /** Sixtieths of a degree. Zero points along +x and the angle turns toward +y. */
      readonly dir: number;
      /** Thousandths of a percent, as written. 100000 is unity. */
      readonly sx: number;
      readonly sy: number;
      /** Sixtieths of a degree. */
      readonly kx: number;
      readonly ky: number;
      readonly algn: RectAlignment;
      readonly rotWithShape: boolean;
    }
  • OuterShadowtype
    interface OuterShadow extends ShadowGeometry {
      readonly kind: 'outerShdw';
      readonly color: Color;
    }
  • InnerShadowtype
    interface InnerShadow extends ShadowGeometry {
      readonly kind: 'innerShdw';
      readonly color: Color;
    }
  • PresetShadowtypea:prstShdw.
    interface PresetShadow {
      readonly kind: 'prstShdw';
      readonly prst: string;
      readonly dist: number;
      readonly dir: number;
      readonly color: Color;
    }
  • Glowtype
    interface Glow {
      readonly kind: 'glow';
      /** EMU. */
      readonly rad: number;
      readonly color: Color;
    }
  • SoftEdgetype
    interface SoftEdge {
      readonly kind: 'softEdge';
      /** EMU. */
      readonly rad: number;
    }
  • Blurtype
    interface Blur {
      readonly kind: 'blur';
      /** EMU. */
      readonly rad: number;
      readonly grow: boolean;
    }
  • Reflectiontypea:reflection. Thirteen attributes, all of which PowerPoint's own nine presets name explicitly, so none of them has to be guessed at. Not modelled further here: 2.8 records what a reflection is and 2.10 decides how to paint one.
    interface Reflection {
      readonly kind: 'reflection';
      readonly blurRad: number;
      readonly stA: number;
      readonly stPos: number;
      readonly endA: number;
      readonly endPos: number;
      readonly dist: number;
      readonly dir: number;
      readonly fadeDir: number;
      readonly sx: number;
      readonly sy: number;
      readonly kx: number;
      readonly ky: number;
      readonly algn: RectAlignment;
      readonly rotWithShape: boolean;
    }
  • Effecttype
    type Effect = OuterShadow | InnerShadow | PresetShadow | Glow | SoftEdge | Blur | Reflection;
  • blurSigmafunctionThe standard deviation for any DrawingML blur radius. Same unit in and out.
    function blurSigma(radius: number): number;
  • glowGeometryfunctionHow far a glow grows the shape and how much it blurs it, both in EMU.
    function glowGeometry(rad: number): {
      grow: number;
      sigma: number;
    };
  • softEdgeGeometryfunctionHow far a soft edge pulls the shape in and how much it blurs it, in EMU.
    function softEdgeGeometry(rad: number): {
      shrink: number;
      sigma: number;
    };
  • shadowOffsetfunctionThe shadow's translation, in EMU.
    function shadowOffset(dist: number, dir: number): {
      dx: number;
      dy: number;
    };
  • Boxtype
    interface Box {
      readonly x: number;
      readonly y: number;
      readonly w: number;
      readonly h: number;
    }
  • anchorPointfunctionThe point of a box that @algn names.
    function anchorPoint(algn: RectAlignment, box: Box): {
      x: number;
      y: number;
    };
  • MatrixtypeA 2-D affine transform, in the order SVG's matrix(a b c d e f) wants them.
    type Matrix = readonly [number, number, number, number, number, number];
  • shadowMatrixfunctionThe whole transform a shadow applies to its shape: scale and skew about the anchor @algn names, then the dist/dir translation.
    function shadowMatrix(shadow: ShadowGeometry, box: Box): Matrix;
  • FilterPrimitivetypeOne SVG filter primitive, as data.
    type FilterPrimitive = {
      readonly op: 'gaussian';
      readonly in: string;
      readonly result: string;
      readonly stdDeviation: number;
    } | {
      readonly op: 'offset';
      readonly in: string;
      readonly result: string;
      readonly dx: number;
      readonly dy: number;
    } | {
      readonly op: 'flood';
      readonly result: string;
      readonly color: string;
      readonly opacity: number;
    } | {
      readonly op: 'composite';
      readonly in: string;
      readonly in2: string;
      readonly result: string;
      readonly operator: 'in' | 'out' | 'over' | 'atop' | 'xor';
    } | {
      readonly op: 'merge';
      readonly inputs: readonly string[];
      readonly result: string;
    } | {
      readonly op: 'morphology';
      readonly in: string;
      readonly result: string;
      readonly operator: 'dilate' | 'erode';
      readonly radius: number;
    } | {
      readonly op: 'matrix';
      readonly in: string;
      readonly result: string;
      readonly matrix: Matrix;
    };
  • FilterGraphtype
    interface FilterGraph {
      /** How far the effects reach outside the shape's own box, in EMU on each side. */
      readonly margin: {
        left: number;
        top: number;
        right: number;
        bottom: number;
      };
      readonly primitives: readonly FilterPrimitive[];
      /** The `result` name holding the finished picture. */
      readonly result: string;
    }
  • effectMarginfunctionHow far one effect paints outside the shape it belongs to, in EMU.
    function effectMargin(effect: Effect, box: Box): {
      left: number;
      top: number;
      right: number;
      bottom: number;
    };
  • shadowBoxfunctionWhere a shadow's own rectangle lands, given its full transform.
    function shadowBox(shadow: ShadowGeometry, box: Box): Box;
  • effectFilterfunctionThe filter graph for one effect list.
    function effectFilter(effects: readonly Effect[], box: Box, colorOf: (color: Color) => {
      css: string;
      alpha: number;
    }): FilterGraph;

Errors

Throws PaintError, whose code is one of:

  • COLOR_HEX
  • COLOR_NUMBER
  • COLOR_SCHEME_NAME
  • COLOR_NO_SCHEME
  • COLOR_NO_PHCLR
  • COLOR_SCHEME_CYCLE
  • COLOR_SYS_UNKNOWN
  • COLOR_PRST_UNKNOWN
  • COLOR_TRANSFORM
  • FILL_NO_STOPS
  • FILL_PATTERN_UNKNOWN
  • LINE_DASH_UNKNOWN
  • LINE_COMPOUND_UNKNOWN
  • LINE_END_UNKNOWN
  • LINE_END_SIZE
  • LINE_DEVICE_SCALE
  • EFFECT_NEGATIVE_RADIUS
  • FILL_BLIP_RESOLUTION
  • FILL_BLIP_CROP
  • FILL_BLIP_SIZE
  • FILL_BLIP_SCALE

Try it live

Reading the deck…

Open the full demo →

Read more

On this page