Files
2026-05-27 23:29:53 +01:00

10 KiB
Raw Permalink Blame History

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", and error instanceof SuaveplanError is 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 detectFormat on a RIFF header whose bytes 8-11 are not WEBP
  • THEN the result is null

Scenario: Detect SVG from textual header

  • WHEN calling detectFormat on a UTF-8 buffer containing <svg or <?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.extractMetadata receives a valid PNG header
  • THEN metadata.width and metadata.height match the IHDR values

Scenario: Parse JPEG SOF0 dimensions after APP0

  • WHEN ImageProcessor.extractMetadata receives JPEG with an APP0 segment before SOF0
  • THEN dimensions are read from the SOF0 segment

Scenario: Handle short BMP data gracefully

  • WHEN ImageProcessor.extractMetadata receives a BMP shorter than 26 bytes
  • THEN metadata.width and metadata.height are 0

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 <= 200 and 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 extractMetadata is called on an 800×600 image with maxPixels: 100
  • THEN it throws ImageError with code IMAGE.TOO_LARGE

Scenario: Emit metadata.extracted event on success

  • WHEN extractMetadata succeeds
  • 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 ImageError with code IMAGE.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 ImageError with code IMAGE.DIMENSIONS_INVALID

Scenario: Invalid crop in PipelineBuilder throws

  • WHEN PipelineBuilder.crop({ x: 0, y: 0, width: -1, height: 10 })
  • THEN it throws ImageError with code IMAGE.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 extractMetadata is called with a valid image
  • THEN image.metadata.extracted_total{format="png",outcome="success"} increments by 1 and image.metadata.extract_duration_ms receives a non-negative observation

Scenario: Span wraps planTransform

  • WHEN planTransform is called
  • THEN a span named image.transform.plan is 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 extractMetadata receives signal that is already aborted
  • THEN it throws ImageError with code IMAGE.PROCESSING_FAILED and cause.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 detectFormat imported
  • THEN ImageProcessor code is not included in the output

Verified by: tests/tree-shake.test.ts::"tree-shakes unused exports"