@compare-ui/core
v0.1.0
Published
Core image utilities for design comparison workflows.
Readme
@compare-ui/core
PNG comparison primitives for design-review workflows.
This package gives you the low-level building blocks to:
- load reference and actual images
- crop images by edge offsets
- compare two images
- generate review artifacts such as overlays and grid rows
- encode the generated images back to PNG buffers
It stays low-level and permissive. Environment-specific behavior belongs in higher-level packages such as the runner, Storybook adapter, and Playwright fixture.
Installation
pnpm add @compare-ui/coreThis package has no peer dependencies.
What it exports
Main functions:
loadImageSource(...)decodePng(...)encodePng(...)prepareImageSource(...)applyCropToSize(...)normalizeImageSize(...)flattenImageBackground(...)createArtifactSet(...)runComparePhase(...)runArtifactInstructions(...)getComparisonMetrics(...)evaluateAcceptance(...)registerImageResourceResolver(...)
Artifact helpers:
artifactRefsoverlayArtifact(...)gridArtifact(...)gridRowArtifact(...)joinArtifact(...)
Browser-safe config entrypoint:
@compare-ui/core/config
Published entrypoints:
@compare-ui/core@compare-ui/core/config
Those entrypoints are intended to resolve from the published build output, and the workspace verifies them through a tarball smoke test.
TypeScript resolver compatibility
The package exports are designed for modern resolver modes such as:
moduleResolution: "bundler"moduleResolution: "node16"moduleResolution: "nodenext"
Older moduleResolution: "node" projects may still run successfully at runtime but can hit TypeScript friction for package subpaths such as @compare-ui/core/config.
If TypeScript reports import-resolution errors for those subpaths, switch to one of the resolver modes above.
Supported image sources
Built-in source types:
fsurlbuffer
Example:
type ImageResource =
| {
type: 'fs';
path: string;
crop?: CropOffsets;
}
| {
type: 'url';
url: string;
crop?: CropOffsets;
}
| {
type: 'buffer';
buffer: Buffer;
crop?: CropOffsets;
};Custom source types can be added by augmenting DesignComparisonResourceMap and registering a resolver.
Cropping
Cropping is offset-based and lossless.
Supported offsets:
toprightbottomleft
Example:
const preparedImage = await prepareImageSource({
type: 'fs',
path: './figma-screen.png',
crop: {
bottom: 20,
},
});If the original image is 320x640, the cropped result becomes 320x620.
Size normalization
Size normalization is an explicit preprocessing step.
Use it when:
- the reference and actual represent the same design at different pixel sizes
- you want to resize before comparison instead of failing on size mismatch
Example:
const normalizedActual = normalizeImageSize(actual.image, {
targetSize: reference.finalSize,
algorithm: 'bilinear',
});Important notes:
- size normalization is opt-in
- size normalization is lossy
- if you do not normalize first,
runComparePhase(...)still expects the images to have identical dimensions
Transparent PNG normalization
Transparent PNG handling is also available as an explicit preprocessing step.
Use flattenImageBackground(...) when:
- the reference image contains transparency
- the browser screenshot is visually composited on a known surface
- you want both images flattened onto the same background before comparison
Example:
const flattenedReference = flattenImageBackground(reference.image, {
color: [255, 255, 255, 255],
});
const flattenedActual = flattenImageBackground(actual.image, {
color: [255, 255, 255, 255],
});Important notes:
- flattening is opt-in
- flattening should use the same background color for both images when visual parity is the goal
- if you do not flatten first, transparent regions are compared as raw RGBA pixels
Comparison
runComparePhase(...) compares reference and actual and returns:
- comparison status
- an updated artifact set
- metrics
- acceptance result
- a diff image when compare is enabled
- accumulated errors
Example compare config:
type AcceptanceThreshold = {
maxDiffPercent: number;
};
type CompareConfig = {
threshold?: number;
includeAntiAliasedPixels?: boolean;
acceptance?: AcceptanceThreshold;
writeDiffArtifact?: boolean;
diffArtifact?: {
alpha?: number;
diffMask?: boolean;
diffColor?: [number, number, number];
diffColorAlt?: [number, number, number];
antiAliasedPixelsColor?: [number, number, number];
};
};
type ComparePhaseStatus = 'passed' | 'failed' | 'error';
type ComparePhaseResult = {
status: ComparePhaseStatus;
artifactSet: {
reference: ImageArtifact;
actual: ImageArtifact;
diff?: ImageArtifact;
overlay?: ImageArtifact;
};
metrics?: ComparisonMetrics;
acceptance?: AcceptanceResult;
errors: string[];
};When writeDiffArtifact: true, the returned artifactSet includes diff. That makes the compare-produced diff available both for encoding and for later artifact instructions.
runComparePhase(...) still compares same-size images. If you want to compare different sizes or normalize transparent edges onto a shared background, do that first with the explicit preprocessing helpers.
Artifact generation
Artifacts are configured separately from comparison.
Available helpers:
overlayArtifact()gridArtifact(...)gridRowArtifact(...)joinArtifact(...)
Public artifact refs:
artifactRefs.referenceartifactRefs.actualartifactRefs.diffartifactRefs.overlay
runArtifactInstructions(...) returns only the artifacts created by the provided instructions. It does not duplicate compare-produced artifacts such as diff; those remain available on the compare result's artifactSet.
Grid configuration supports:
sizeoffsetXoffsetYcoloralphalineWidth
Example:
gridRowArtifact({
items: [artifactRefs.overlay, artifactRefs.actual, artifactRefs.reference],
size: 16,
lineWidth: 2,
});Stable default artifact names:
diffoverlaygridgrid-rowjoin
Those names become file names such as diff.png, overlay.png, and grid-row.png when you encode or persist the images.
grid-row.png is a composite debugging artifact, not source truth. When a seam or corner looks suspicious in the composite, inspect these in order:
actual.pngreference.pngoverlay.pngorgrid-row.png
That avoids mistaking artifact chrome for a real UI defect.
Basic example
import {
artifactRefs,
createArtifactSet,
encodePng,
flattenImageBackground,
gridRowArtifact,
normalizeImageSize,
overlayArtifact,
prepareImageSource,
runArtifactInstructions,
runComparePhase,
} from '@compare-ui/core';
const reference = await prepareImageSource({
type: 'fs',
path: './reference.png',
crop: {
bottom: 20,
},
});
const actual = await prepareImageSource({
type: 'fs',
path: './actual.png',
});
const flattenedReference = flattenImageBackground(reference.image, {
color: [255, 255, 255, 255],
});
const flattenedActual = flattenImageBackground(actual.image, {
color: [255, 255, 255, 255],
});
const normalizedActual = normalizeImageSize(flattenedActual, {
targetSize: reference.finalSize,
algorithm: 'bilinear',
});
const artifactSet = createArtifactSet({
reference: flattenedReference,
actual: normalizedActual,
});
const comparePhaseResult = runComparePhase(artifactSet, {
threshold: 0.1,
includeAntiAliasedPixels: false,
writeDiffArtifact: true,
acceptance: {
maxDiffPercent: 0.5,
},
});
const artifactPhaseResult = runArtifactInstructions(comparePhaseResult.artifactSet, [
overlayArtifact(),
gridRowArtifact({
items: [artifactRefs.diff, artifactRefs.overlay, artifactRefs.actual, artifactRefs.reference],
size: 16,
lineWidth: 2,
}),
]);
const diffBytes = comparePhaseResult.artifactSet.diff ? encodePng(comparePhaseResult.artifactSet.diff) : undefined;
const gridRow = artifactPhaseResult.artifacts.find((artifact) => artifact.name === 'grid-row');
const gridRowBytes = gridRow ? encodePng(gridRow.image) : undefined;Custom resource loaders
If you need another source type, extend the resource map and register a resolver.
There are two parts to this:
- type registration in TypeScript through module augmentation
- runtime registration through
registerImageResourceResolver(...)
Type registration makes the new type valid in config and gives you autocomplete.
Runtime registration tells @compare-ui/core how to load that resource.
Step 1. Define a resource type:
type S3ImageResourceDefinition = {
bucket: string;
key: string;
region?: string;
crop?: CropOffsets;
};Step 2. Augment DesignComparisonResourceMap:
declare module '@compare-ui/core' {
interface DesignComparisonResourceMap {
s3: S3ImageResourceDefinition;
}
}Step 3. Register a resolver for that resource type:
registerImageResourceResolver({
type: 's3',
async resolve(resource) {
return {
resourceType: 's3',
bytes: await loadFromS3(resource),
sourceLabel: `${resource.bucket}/${resource.key}`,
};
},
});After registration, type: 's3' becomes a valid resource in the same places where built-in resources are accepted.
If you are publishing a separate package, that package should:
- export the resource type
- include the module augmentation in its type declarations
- export a resolver or resolver factory for runtime registration
This pattern is intended for:
- internal asset services
- cloud storage
- vendor integrations
- package-based extensions such as
@compare-ui/figma
Notes
- This package works with PNG images.
- It returns in-memory results and encoded buffers.
- Use
@compare-ui/core/configin browser-side code such as Storybook stories when you only need config helpers likeartifactRefs,overlayArtifact(...), orgridRowArtifact(...). - It is intended to be used directly or through higher-level packages such as the runner, Playwright fixture, and Storybook integration.
