10 KiB
Capability Spec: image
Package: @suaveplan/image
Status: STABLE
Every requirement below carries a
**Verified by:**line at authoring time.
ADDED Requirements
Requirement: Stable error code registry via defineCodes
The package SHALL declare a single defineCodes("IMAGE", [...]) registry named IMAGE_CODES containing every error code emitted by the package. No hand-typed string literals for codes SHALL appear in src/ or tests.
Scenario: Code values use IMAGE.* namespace
- WHEN a consumer reads
IMAGE_CODES.NOT_FOUND - THEN the value is exactly
"IMAGE.NOT_FOUND"
Scenario: Every code is listed in the registry
- WHEN introspecting
IMAGE_CODES - THEN it contains
NOT_FOUND,DECODE_FAILED,ENCODE_FAILED,FORMAT_UNSUPPORTED,TOO_LARGE,PROCESSING_FAILED,DIMENSIONS_INVALID
Verified by: src/error-codes/error-codes.test.ts::"code values use IMAGE.* namespace" AND src/error-codes/error-codes.test.ts::"contains expected codes"
Requirement: Typed error hierarchy extending SuaveplanError
The package SHALL export ImageError extending SuaveplanError<ImageCode> with an optional ImageErrorContext carrying format, width, height, pixels, maxPixels, and path. Typed factory functions SHALL exist for every failure mode.
Scenario: ImageError carries code, message, context, and cause
- WHEN constructing
new ImageError(IMAGE_CODES.FORMAT_UNSUPPORTED, "msg", { format: "heic" }) - THEN
error.code === "IMAGE.FORMAT_UNSUPPORTED",error.context.format === "heic", anderror instanceof SuaveplanErroris true
Scenario: Factory functions produce correctly-coded errors
- WHEN calling
imageNotFound("/a.png"),imageDecodeFailed("png"),imageTooLarge(10, 5),imageDimensionsInvalid(-1, 0) - THEN each result has the matching
IMAGE.*code and populated context
Verified by: src/errors/errors.test.ts::"creates error with code and message" AND src/errors/errors.test.ts::"factory functions produce correctly-coded errors"
Requirement: Magic-byte format detection
The package SHALL detect image formats from the first bytes of a Uint8Array for PNG, JPEG, GIF, BMP, TIFF (little- and big-endian), ICO, WebP, and SVG. Unknown headers SHALL return null.
Scenario: Detect PNG from header
- WHEN calling
detectFormat(new Uint8Array([0x89, 0x50, 0x4e, 0x47])) - THEN the result is
"png"
Scenario: Reject RIFF without WEBP tag
- WHEN calling
detectFormaton a RIFF header whose bytes 8-11 are notWEBP - THEN the result is
null
Scenario: Detect SVG from textual header
- WHEN calling
detectFormaton a UTF-8 buffer containing<svgor<?xml - THEN the result is
"svg"
Verified by: src/types/image-types.test.ts::"detects PNG" AND src/types/image-types.test.ts::"rejects RIFF without WEBP tag" AND src/types/image-types.test.ts::"detects SVG"
Requirement: Dimension parsing for supported formats
The package SHALL parse width and height from PNG, JPEG, GIF, and BMP headers. TIFF SHALL report format but dimensions zero (not yet parsed). Short or malformed data SHALL yield { width: 0, height: 0 } without throwing.
Scenario: Parse PNG IHDR dimensions
- WHEN
ImageProcessor.extractMetadatareceives a valid PNG header - THEN
metadata.widthandmetadata.heightmatch the IHDR values
Scenario: Parse JPEG SOF0 dimensions after APP0
- WHEN
ImageProcessor.extractMetadatareceives JPEG with an APP0 segment before SOF0 - THEN dimensions are read from the SOF0 segment
Scenario: Handle short BMP data gracefully
- WHEN
ImageProcessor.extractMetadatareceives a BMP shorter than 26 bytes - THEN
metadata.widthandmetadata.heightare0
Verified by: src/processor/processor.test.ts::"extracts PNG metadata" AND src/processor/processor.test.ts::"handles JPEG with multiple segments before SOF" AND src/processor/processor.test.ts::"handles short BMP data"
Requirement: Resize math with fit modes
The package SHALL calculate output dimensions from ResizeOptions supporting cover, contain, fill, inside, outside fits, plus withoutEnlargement. Omitting both width and height SHALL return original dimensions.
Scenario: Contain fit scales down to fit both axes
- WHEN
calculateResize({ width: 800, height: 600 }, { width: 200, height: 200, fit: "contain" }) - THEN result width and height are both
<= 200and maintain aspect ratio
Scenario: WithoutEnlargement prevents upscaling
- WHEN
calculateResize({ width: 200, height: 150 }, { width: 800, withoutEnlargement: true }) - THEN result is
{ width: 200, height: 150 }
Verified by: src/types/image-types.test.ts::"contain fit scales down" AND src/types/image-types.test.ts::"withoutEnlargement prevents upscaling"
Requirement: Crop region validation
The package SHALL reject crop regions with negative origin, zero or negative size, or bounds exceeding the source dimensions.
Scenario: Reject crop exceeding image width
- WHEN
validateCrop({ width: 800, height: 600 }, { x: 700, y: 0, width: 200, height: 100 }) - THEN the result is
false
Verified by: src/types/image-types.test.ts::"rejects crop exceeding width"
Requirement: ImageProcessor metadata extraction with limits and events
The package SHALL provide ImageProcessor with configurable maxPixels, defaultQuality, and defaultFormat. extractMetadata SHALL emit "metadata.extracted" via the internal event bus and throw IMAGE_CODES.TOO_LARGE when pixel count exceeds the limit.
Scenario: Throw when image exceeds maxPixels
- WHEN
extractMetadatais called on an 800×600 image withmaxPixels: 100 - THEN it throws
ImageErrorwith codeIMAGE.TOO_LARGE
Scenario: Emit metadata.extracted event on success
- WHEN
extractMetadatasucceeds - THEN the
"metadata.extracted"event fires with the metadata object
Verified by: src/processor/processor.test.ts::"throws for image exceeding maxPixels" AND src/processor/processor.test.ts::"emits metadata.extracted event"
Requirement: Transform pipeline planning
The package SHALL provide PipelineBuilder with a fluent API for resize, crop, rotate, flip, quality, and toFormat. ImageProcessor.planTransform SHALL compute final dimensions by applying operations in order and emit "transform.planned".
Scenario: 90-degree rotation swaps dimensions
- WHEN planning a transform with
rotate(90)on an 800×600 image - THEN result dimensions are 600×800
Scenario: Invalid crop region throws processing error
- WHEN planning a transform with a crop region that exceeds current dimensions
- THEN it throws
ImageErrorwith codeIMAGE.PROCESSING_FAILED
Verified by: src/processor/processor.test.ts::"applies 90-degree rotation" AND src/processor/processor.test.ts::"throws for invalid crop"
Requirement: Zod schema validation on public boundaries
The package SHALL validate ImageProcessorConfig, CropRegion, ResizeOptions, TransformPipeline, and TransformOp via Zod schemas at public entrypoints. Invalid input SHALL throw IMAGE_CODES.DIMENSIONS_INVALID (or IMAGE_CODES.PROCESSING_FAILED where semantically appropriate).
Scenario: Invalid config rejects negative maxPixels
- WHEN constructing
ImageProcessor({ maxPixels: -1 }) - THEN it throws
ImageErrorwith codeIMAGE.DIMENSIONS_INVALID
Scenario: Invalid crop in PipelineBuilder throws
- WHEN
PipelineBuilder.crop({ x: 0, y: 0, width: -1, height: 10 }) - THEN it throws
ImageErrorwith codeIMAGE.DIMENSIONS_INVALID
Verified by: src/processor/processor.spec-conformance.test.ts::"rejects invalid config" AND src/processor/processor.spec-conformance.test.ts::"rejects negative crop dimensions"
Requirement: OpenTelemetry instrumentation
The package SHALL emit OTel counters (image.metadata.extracted_total, image.transform.planned_total) and histograms (image.metadata.extract_duration_ms, image.transform.plan_duration_ms) from production code paths. Spans SHALL wrap extractMetadata and planTransform.
Scenario: Extract metadata increments counter and records histogram
- WHEN
extractMetadatais called with a valid image - THEN
image.metadata.extracted_total{format="png",outcome="success"}increments by 1 andimage.metadata.extract_duration_msreceives a non-negative observation
Scenario: Span wraps planTransform
- WHEN
planTransformis called - THEN a span named
image.transform.planis started and ended
Verified by: src/processor/processor.spec-conformance.test.ts::"increments extracted counter on success" AND src/processor/processor.spec-conformance.test.ts::"creates planTransform span"
Requirement: AbortSignal propagation
The package SHALL accept an optional AbortSignal on async and synchronous entrypoints. A pre-aborted signal SHALL short-circuit before work begins with IMAGE_CODES.PROCESSING_FAILED and cause set to AbortError.
Scenario: Pre-aborted signal short-circuits extractMetadata
- WHEN
extractMetadatareceivessignalthat is already aborted - THEN it throws
ImageErrorwith codeIMAGE.PROCESSING_FAILEDandcause.name === "AbortError"
Verified by: src/processor/processor.spec-conformance.test.ts::"short-circuits on pre-aborted signal"
Requirement: Subpath exports and sideEffects false
The package SHALL declare sideEffects: false and provide subpath exports for ./error-codes/error-codes, ./errors/errors, ./types/image-types, and ./processor/processor. Feature-module barrels SHALL use explicit named re-exports.
Scenario: Tree-shake test passes
- WHEN bundling with only
detectFormatimported - THEN
ImageProcessorcode is not included in the output
Verified by: tests/tree-shake.test.ts::"tree-shakes unused exports"