6.1 KiB
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
ImageAdapterinterface — all codec-specific work delegated;ImageProcessoris codec-agnostic.MemoryImageAdapter— pure-JS pixel operations (resize nearest-neighbour, crop, flip, rotate 90/180/270) operating on raw RGBAUint8Array. No external deps. Used in tests and as a fallback.- Format detection — delegates to
@suaveplan/mimefor initial MIME sniff; headers re-verified insidereadMetadata. AbortSignalpropagation — every async method passes signal through; aborted operations throwImageError(IMAGE_OPERATION_FAILED).- OTel instrumentation — span
image.process, counterimage.operation_total(attrimage.operation), histogramimage.duration_ms, attributesimage.format,image.width,image.height. - Property-based tests — crop idempotency (crop same region twice = same result), resize monotonicity (larger target → larger output), flip involution (flip twice = original).
- Benchmark suite —
metadataread,resize1920×1080→640×360,convertPNG→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.