Files
suaveplan/openspec/changes/enterprise-image/proposal.md
2026-05-27 23:29:53 +01:00

6.1 KiB
Raw Blame History

OpenSpec Change: enterprise-image

  • Change ID: enterprise-image
  • Package: @suaveplan/image
  • Category: universal
  • Target path: universal/media/image
  • Status: PROPOSED
  • Date: 2026-04-24
  • Reference spec: /mnt/s/web/suaveplan/enterprise/image.md

Summary

Deliver @suaveplan/image on the genesis branch from scratch at enterprise quality. The package provides image metadata extraction, format detection, and transformation operations: resize, crop, rotate, flip, format conversion, quality adjustment, and channel extraction. Pure TypeScript with no Node.js or browser hard deps — pixel manipulation operates on raw Uint8Array buffers. Pluggable codec adapters (e.g. sharp, native WebCodecs) via the ImageAdapter interface.


Current-state audit (genesis)

The package does not exist on genesis yet — this is a fresh build. No prior source, no test results, no coverage data.


Available workspace deps on genesis

Package Role
@suaveplan/error (workspace:^) Base class for ImageError; defineCodes for IMAGE.* codes
@suaveplan/types (workspace:^) LoggerLike, Clock
@suaveplan/mime (workspace:^) MIME/format detection for input buffers
@suaveplan/crypto-utils (workspace:^) Content-hash cache keys
zod (catalog:forms) Zod schemas for all public option types
@opentelemetry/api (catalog:observability) Spans + counters on every operation

Dev deps: vitest, @vitest/coverage-istanbul, fast-check, mitata (all catalog:testing).


Public API

Core types

class ImageError extends SuaveplanError

type ImageFormat = 'jpeg' | 'png' | 'webp' | 'avif' | 'gif' | 'bmp' | 'tiff' | 'svg' | 'unknown'
type ColorSpace  = 'srgb' | 'p3' | 'cmyk' | 'grayscale'

interface ImageMetadata {
  width: number
  height: number
  format: ImageFormat
  colorSpace: ColorSpace
  hasAlpha: boolean
  byteSize: number
  mimeType: string
}

interface ResizeOptions {
  width?: number
  height?: number
  fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside'
  position?: 'center' | 'top' | 'bottom' | 'left' | 'right'
  withoutEnlargement?: boolean
}

interface CropOptions {
  left: number
  top: number
  width: number
  height: number
}

interface ConvertOptions {
  format: ImageFormat
  quality?: number          // 1–100
  lossless?: boolean
}

interface ImageAdapter {
  readonly supportedFormats: readonly ImageFormat[]
  readMetadata(data: Uint8Array): Promise<ImageMetadata>
  resize(data: Uint8Array, options: ResizeOptions): Promise<Uint8Array>
  crop(data: Uint8Array, options: CropOptions): Promise<Uint8Array>
  rotate(data: Uint8Array, degrees: number): Promise<Uint8Array>
  flip(data: Uint8Array, axis: 'horizontal' | 'vertical'): Promise<Uint8Array>
  convert(data: Uint8Array, options: ConvertOptions): Promise<Uint8Array>
}

interface ImageProcessorOptions {
  adapter: ImageAdapter
  logger?: LoggerLike
  clock?: Clock
}

class ImageProcessor {
  constructor(options: ImageProcessorOptions)
  metadata(data: Uint8Array, signal?: AbortSignal): Promise<ImageMetadata>
  resize(data: Uint8Array, options: ResizeOptions, signal?: AbortSignal): Promise<Uint8Array>
  crop(data: Uint8Array, options: CropOptions, signal?: AbortSignal): Promise<Uint8Array>
  rotate(data: Uint8Array, degrees: number, signal?: AbortSignal): Promise<Uint8Array>
  flip(data: Uint8Array, axis: 'horizontal' | 'vertical', signal?: AbortSignal): Promise<Uint8Array>
  convert(data: Uint8Array, options: ConvertOptions, signal?: AbortSignal): Promise<Uint8Array>
}

function createImageProcessor(options: ImageProcessorOptions): ImageProcessor

Error codes

type ImageErrorCode =
  | 'IMAGE_UNSUPPORTED_FORMAT'
  | 'IMAGE_INVALID_DIMENSIONS'
  | 'IMAGE_DECODE_FAILED'
  | 'IMAGE_ENCODE_FAILED'
  | 'IMAGE_OPERATION_FAILED'
  | 'IMAGE_INVALID_OPTIONS'

const IMAGE_ERROR_METADATA: Readonly<Record<ImageErrorCode, CodeMetadata>>
function registerImageErrorCodes(): void

Zod schemas

ResizeOptionsSchema
CropOptionsSchema
ConvertOptionsSchema
ImageMetadataSchema
ImageProcessorOptionsSchema

New additions for genesis

  1. ImageAdapter interface — all codec-specific work delegated; ImageProcessor is codec-agnostic.
  2. MemoryImageAdapter — pure-JS pixel operations (resize nearest-neighbour, crop, flip, rotate 90/180/270) operating on raw RGBA Uint8Array. No external deps. Used in tests and as a fallback.
  3. Format detection — delegates to @suaveplan/mime for initial MIME sniff; headers re-verified inside readMetadata.
  4. AbortSignal propagation — every async method passes signal through; aborted operations throw ImageError(IMAGE_OPERATION_FAILED).
  5. OTel instrumentation — span image.process, counter image.operation_total (attr image.operation), histogram image.duration_ms, attributes image.format, image.width, image.height.
  6. Property-based tests — crop idempotency (crop same region twice = same result), resize monotonicity (larger target → larger output), flip involution (flip twice = original).
  7. Benchmark suite — metadata read, resize 1920×1080→640×360, convert PNG→WebP.

File layout

universal/media/image/
  package.json
  tsconfig.json
  vitest.config.ts
  src/
    index.ts
    error-codes.ts
    constants/
      index.ts
    types/
      common.ts
      common.md
    core/
      image-processor/
        image-processor.ts
        image-processor.md
        image-processor.types.ts
        image-processor.test.ts
        index.ts
    adapters/
      memory-adapter/
        memory-adapter.ts
        memory-adapter.md
        memory-adapter.test.ts
        index.ts
    utils/
      format-detect.ts
      format-detect.md
      format-detect.test.ts
    __benchmarks__/
      image.bench.ts
  tests/
    integration.test.ts
    property.test.ts

Impact

Downstream: @suaveplan/media facade, @suaveplan/compress-image, @suaveplan/image-quantisation. Breaking changes: none — fresh build. Prerequisites: @suaveplan/error, @suaveplan/types, @suaveplan/mime, @suaveplan/crypto-utils published to Verdaccio.