paint
Colour, fills, strokes and effects, each rule measured against PowerPoint rather than read from the standard.
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
// <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';
PaintErrorclassclass 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 seeclamp01for 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;
clamp01functionfunction 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-rangesandlare 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;
parseHexfunctionSplit4472C4into 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 whosevalis aST_Percentagefamily 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 whosevalis an angle: sixtieths of a degree.type AngleTransformOp = 'hue' | 'hueOff';
FlagTransformOptypeTransforms with no attribute at all.type FlagTransformOp = 'comp' | 'inv' | 'gray' | 'gamma' | 'invGamma';
ColorTransformOptypetype 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 ofEG_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 ofa:clrScheme, and the only things aclrMapmaps *to*.type SchemeSlot = 'dk1' | 'lt1' | 'dk2' | 'lt2' | 'accent1' | 'accent2' | 'accent3' | 'accent4' | 'accent5' | 'accent6' | 'hlink' | 'folHlink';
SCHEME_SLOTSconstconst SCHEME_SLOTS: readonly SchemeSlot[];
MappedColorNametypeThe twelve attributes ofp: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_NAMESconstconst MAPPED_COLOR_NAMES: readonly MappedColorName[];
SchemeColorNametypeEvery valuea:schemeClr/@valmay take: seventeen, not twelve and not sixteen.type SchemeColorName = SchemeSlot | MappedColorName | 'phClr';
ClrSchemetypeA parseda:clrScheme. Each slot holds a colour, not a hex string - see below.type ClrScheme = Readonly<Record<SchemeSlot, Color>>;
ClrMaptypeA parsedp:clrMaporp: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;ais 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 aColorto 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 aa:schemeClr/@valnames.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
parsePercentagefunctionAST_Percentageattribute, 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_RECTconstconst WHOLE_RECT: RelativeRect;
TileFlipModetypeST_TileFlipMode.type TileFlipMode = 'none' | 'x' | 'y' | 'xy';
GradientStoptypeinterface 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; }GradientShadetypetype GradientShade = LinearShade | PathShade;
GradientFilltypeinterface 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; }PatternFilltypeinterface 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; }SolidFilltypeinterface SolidFill { readonly type: 'solid'; readonly color: Color; }NoFilltypeinterface NoFill { readonly type: 'none'; }GroupFilltypea:grpFill- notnoFill.interface GroupFill { readonly type: 'group'; }TileAligntypeWherea:tileputs one whole tile against the shape box. ADR 0036.type TileAlign = 'tl' | 't' | 'tr' | 'l' | 'ctr' | 'r' | 'bl' | 'b' | 'br';
BlipEffecttypeOnea:blipcolour 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; }Filltypetype 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 att.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 attalong a gradient - what a viewer will actually paint.function gradientColorAt(fill: GradientFill, t: number, ctx?: ColorContext): Rgba;
GradientVectortypeinterface 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 aCT_RelativeRect's insets into a rectangle in 0..1 shape fractions.function insetRect(rect: RelativeRect): { l: number; t: number; r: number; b: number; };PathGeometrytypeinterface 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; }BlipPlacementtypetype BlipPlacement = StretchPlacement | TilePlacement;
BLIP_LUMAconstBT.709, which is whata:grayscl,a:biLevelanda:duotoneall weigh by.const BLIP_LUMA: { readonly r: 0.2126; readonly g: 0.7152; readonly b: 0.0722; };contrastScalefunctionThe slopea:lum/@contrastapplies about mid-grey.function contrastScale(contrast: number): number;
brightOffsetfunctionWhata:lum/@brightadds, 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_TILESconstconst PATTERN_TILES: Readonly<Record<string, PatternTile>>;
PRESET_PATTERN_NAMESconstThe 54 names, inMsoPatternTypeorder - 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
ResolvedPatterntypeinterface 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 ofa: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_NAMESconstconst COMPOUND_NAMES: readonly string[];
isCompoundNamefunctionDeliberately not a type predicate; seeisPresetDashNamefor 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_DASHESconstconst PRESET_DASHES: Readonly<Record<string, readonly DashSegment[]>>;
PRESET_DASH_NAMESconstThe elevenST_PresetLineDashValnames, 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';
Markertypeinterface 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@lenand@w, in multiples of the stroke width.const MARKER_SIZE: Readonly<Record<string, number>>;
MARKER_SIZE_NAMESconstconst MARKER_SIZE_NAMES: readonly string[];
MARKERSconstconst MARKERS: Readonly<Record<string, Marker>>;
MARKER_TYPE_NAMESconstconst MARKER_TYPE_NAMES: readonly string[];
isMarkerTypeNamefunctionDeliberately not a type predicate; seeisPresetDashNamefor 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 ana:lnthat does not say.const DEFAULT_LINE_WIDTH = 9525;
DEFAULT_LINE_CAPconstThe cap of ana:lnthat does not say.const DEFAULT_LINE_CAP: LineCap;
DEFAULT_LINE_JOINconstThe join of ana:lnwith no join element.const DEFAULT_LINE_JOIN: LineJoinKind;
DEFAULT_MITER_LIMITconstThe miter limit of ana:miterwith no@lim, as a multiple of the width.const DEFAULT_MITER_LIMIT = 8;
LineCaptypetype LineCap = 'flat' | 'sq' | 'rnd';
LineJoinKindtypetype LineJoinKind = 'round' | 'bevel' | 'miter';
PenAlignmenttypetype PenAlignment = 'ctr' | 'in';
LineEndSizetypetype LineEndSize = 'sm' | 'med' | 'lg';
LineJointypeinterface LineJoin { readonly kind: LineJoinKind; /** A multiple of the stroke width. `null` means the attribute was absent. */ readonly limit: number | null; }LineEndtypea:headEndora:tailEnd.interface LineEnd { readonly type: string; readonly w: LineEndSize; readonly len: LineEndSize; }LineDashtypetype 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; }ResolvedLinetypeinterface 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'sstroke-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; }markerGeometryfunctionfunction 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 atpxPerPtdevice pixels to the point.function devicePen(widthEmu: number, pxPerPt: number): DevicePen;
deviceStrokeWidthfunctionThe width a stroke is drawn at, in EMU, atpxPerPtdevice 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 atpxPerPt(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 ofr.const EDGE_GROWTH_PER_RADIUS = 0.9;
GLOW_RADIUS_SCALEconstA glow uses half the radius it declares.const GLOW_RADIUS_SCALE = 0.5;
RectAlignmenttypetype RectAlignment = 'tl' | 't' | 'tr' | 'l' | 'ctr' | 'r' | 'bl' | 'b' | 'br';
DEFAULT_SHADOW_ALIGNconstThe anchor a shadow's scale and skew are applied about, when@algnis absent.const DEFAULT_SHADOW_ALIGN: RectAlignment;
ShadowGeometrytypeinterface 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; }OuterShadowtypeinterface OuterShadow extends ShadowGeometry { readonly kind: 'outerShdw'; readonly color: Color; }InnerShadowtypeinterface 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; }Glowtypeinterface Glow { readonly kind: 'glow'; /** EMU. */ readonly rad: number; readonly color: Color; }SoftEdgetypeinterface SoftEdge { readonly kind: 'softEdge'; /** EMU. */ readonly rad: number; }Blurtypeinterface 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; }Effecttypetype 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; };Boxtypeinterface Box { readonly x: number; readonly y: number; readonly w: number; readonly h: number; }anchorPointfunctionThe point of a box that@algnnames.function anchorPoint(algn: RectAlignment, box: Box): { x: number; y: number; };MatrixtypeA 2-D affine transform, in the order SVG'smatrix(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@algnnames, then thedist/dirtranslation.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; };FilterGraphtypeinterface 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…