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

195 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```ts
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
```ts
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
```ts
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
```text
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.