hazo_images 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGE_LOG.md ADDED
@@ -0,0 +1,52 @@
1
+ # hazo_images Change Log
2
+
3
+ ## 1.1.0 (2026-05-29) — Wave 2 standardisation
4
+
5
+ ### Changed
6
+ - Error classes now extend `HazoError` subclasses from `hazo_core`:
7
+ - `ImageProcessingError` → extends `HazoExternalError` (`HAZO_IMAGES_PROCESSING_FAILED`, HTTP 502, retryable)
8
+ - `UnsupportedFormatError` → extends `HazoValidationError` (`HAZO_IMAGES_UNSUPPORTED_FORMAT`, HTTP 400)
9
+ - Existing class names + public API preserved — `instanceof` checks and `errorType` discriminators keep working.
10
+ - `processImage` and `uploadProcessedImage` emit structured `log.debug` events
11
+ (`process_image.started`, `process_image.completed`,
12
+ `upload_processed_image.completed`) via `hazo_core.createLogger`.
13
+ Correlation ID is inherited from the caller's AsyncLocalStorage context.
14
+ - `package.json`: `hazo_core@^1.0.0` added as a required peer dependency.
15
+ Engines block removed (workspace standard). `dist/` files list now
16
+ includes `config/`.
17
+
18
+ ### Added
19
+ - `SharpMissingError` extends `HazoConfigError` (`HAZO_IMAGES_CONFIG_SHARP_MISSING`)
20
+ thrown when the optional `sharp` peer dep is not installed at first
21
+ `processImage` call. Replaces the previous bare `Error` throw.
22
+ - `ImageUploadError` extends `HazoExternalError` (`HAZO_IMAGES_UPLOAD_FAILED`)
23
+ thrown when a FileManager rejects a buffer upload during
24
+ `uploadProcessedImage`. Replaces the previous bare `Error` throws and
25
+ carries `{ virtualPath, fileManagerError }` in `context`.
26
+ - Workspace-standard INI config under `config/hazo_images_config.ini.sample`
27
+ with `[general]`, `[log.overrides]`, `[processing]` sections. Loader at
28
+ `src/lib/config/hazo_images_config.ts::getImagesConfig()` wraps
29
+ `hazo_core.loadConfig` with a Zod schema. Env-var overrides:
30
+ `HAZO_IMAGES_<SECTION>_<KEY>`.
31
+ - `src/utils/logger.ts` — `get_logger()` / `set_logger()` defaulting to
32
+ `createLogger('hazo_images')` from `hazo_core`.
33
+ - `src/utils/uuid.ts` — `generateUUID()` wrapping
34
+ `generateRequestId().slice(4)` so all IDs trace through `hazo_core`.
35
+ - `AGENTS.md` at package root + `src/` + `test-app/` (S13).
36
+ - `design/feature_requests/INDEX.md` (S16).
37
+
38
+ ### Notes
39
+ - No outbound HTTP calls — `fetchWithRequestId` wiring not required.
40
+ - No database tables owned — no `db_setup_*.sql` files.
41
+ - `hazo_images` is in the "Doesn't adopt hazo_api" column per the
42
+ ecosystem map; no envelope/route wiring in src/.
43
+
44
+ ## 1.0.0 (2026-05-21)
45
+
46
+ ### Added
47
+ - Initial release: server-side image pipeline
48
+ - `processImage(buffer, opts)` — Sharp wrapper with lazy load, EXIF strip, auto-rotate, resize, thumbnail generation
49
+ - `uploadProcessedImage(fm, buffer, path, opts)` — orchestration helper composing with hazo_files FileManager
50
+ - Subpath exports: `hazo_images/server` (pipeline), `hazo_images/ui` (reserved for @1.1+)
51
+ - `ImageProcessingError` and `UnsupportedFormatError` error types
52
+ - Sharp is an optional peer dep — importing the package without Sharp installed does not crash
package/README.md ADDED
@@ -0,0 +1,166 @@
1
+ # hazo_images
2
+
3
+ Server-side image processing pipeline for the hazo ecosystem. Sharp wrapper with EXIF handling, auto-rotate, resize, and thumbnail generation. Composes with `hazo_files` via an integration helper.
4
+
5
+ Image **storage** is owned by `hazo_files`. This package handles **processing only**.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install hazo_images
11
+
12
+ # Required peer dep — provides errors, correlation IDs, logger, config loader
13
+ npm install hazo_core
14
+
15
+ # Required peer deps for image processing
16
+ npm install sharp
17
+
18
+ # Optional — only needed if using uploadProcessedImage
19
+ npm install hazo_files@^2.1.1
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ### processImage — standalone pipeline primitive
25
+
26
+ ```ts
27
+ import { processImage } from 'hazo_images/server';
28
+
29
+ const fs = await import('node:fs/promises');
30
+ const buffer = await fs.readFile('./photo.jpg');
31
+
32
+ const result = await processImage(buffer, {
33
+ stripExif: true, // default: true — privacy-safe
34
+ autoRotate: true, // default: true — corrects EXIF orientation
35
+ maxDimension: 4096, // default: 4096 — resize longest side, keep AR
36
+ webp: false, // default: false — preserve original format
37
+ thumbnails: [256, 512, 1024],
38
+ });
39
+
40
+ console.log(result.metadata);
41
+ // { width: 2400, height: 1600, format: 'jpeg' }
42
+
43
+ console.log(result.thumbnails.length);
44
+ // 3 — one per requested size
45
+
46
+ // result.buffer — processed main image as Buffer
47
+ // result.thumbnails[0] — { size: 256, buffer: Buffer, format: 'jpeg' }
48
+ ```
49
+
50
+ ### uploadProcessedImage — compose with hazo_files
51
+
52
+ ```ts
53
+ import { uploadProcessedImage } from 'hazo_images/server';
54
+ import { createInitializedFileManager } from 'hazo_files/server';
55
+
56
+ const fm = await createInitializedFileManager({ config: { provider: 'local', local: { basePath: './uploads' } } });
57
+ const buffer = await fs.readFile('./photo.jpg');
58
+
59
+ const result = await uploadProcessedImage(fm, buffer, '/photos/wedding.jpg', {
60
+ thumbnails: [256, 512, 1024],
61
+ stripExif: true,
62
+ autoRotate: true,
63
+ maxDimension: 4096,
64
+ webp: false,
65
+ });
66
+
67
+ console.log(result.main); // FileItem — the processed main image
68
+ console.log(result.thumbs); // { 256: FileItem, 512: FileItem, 1024: FileItem }
69
+ ```
70
+
71
+ Thumbnails are named `{basename}__thumb_{size}.{ext}` and stored in the same directory as the main file:
72
+ ```
73
+ /photos/wedding.jpg
74
+ /photos/wedding__thumb_256.jpg
75
+ /photos/wedding__thumb_512.jpg
76
+ /photos/wedding__thumb_1024.jpg
77
+ ```
78
+
79
+ ### WebP transcoding
80
+
81
+ ```ts
82
+ const result = await processImage(buffer, {
83
+ webp: true,
84
+ thumbnails: [256, 512],
85
+ });
86
+ // result.metadata.format === 'webp'
87
+ // result.thumbnails[0].format === 'webp'
88
+ // thumbnail extension in uploadProcessedImage: __thumb_256.webp
89
+ ```
90
+
91
+ ## Error Types
92
+
93
+ All errors extend `HazoError` subclasses from `hazo_core` (Wave 2). The
94
+ legacy class names are preserved so existing `instanceof` checks and
95
+ `errorType` discriminators keep working.
96
+
97
+ ```ts
98
+ import {
99
+ ImageProcessingError,
100
+ ImageUploadError,
101
+ SharpMissingError,
102
+ UnsupportedFormatError,
103
+ } from 'hazo_images';
104
+ import { HazoError } from 'hazo_core';
105
+
106
+ try {
107
+ const result = await processImage(badBuffer);
108
+ } catch (err) {
109
+ if (err instanceof UnsupportedFormatError) {
110
+ // Buffer was not a recognized image format (e.g. PDF, text)
111
+ console.error('Not an image:', err.message);
112
+ } else if (err instanceof ImageProcessingError) {
113
+ // Sharp failed during processing
114
+ console.error('Processing failed:', err.message);
115
+ console.error('Original error:', err.originalError);
116
+ } else if (HazoError.is(err)) {
117
+ // Any other hazo_images error — read `err.code` to discriminate
118
+ console.error(err.code, err.message);
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### Error reference
124
+
125
+ | Class | Extends | Code | HTTP | When |
126
+ |---|---|---|---|---|
127
+ | `UnsupportedFormatError` | `HazoValidationError` | `HAZO_IMAGES_UNSUPPORTED_FORMAT` | 400 | Buffer is not a recognized image format |
128
+ | `ImageProcessingError` | `HazoExternalError` | `HAZO_IMAGES_PROCESSING_FAILED` | 502 | Sharp failed during processing or thumbnail generation |
129
+ | `SharpMissingError` | `HazoConfigError` | `HAZO_IMAGES_CONFIG_SHARP_MISSING` | n/a | Optional `sharp` peer dep is not installed |
130
+ | `ImageUploadError` | `HazoExternalError` | `HAZO_IMAGES_UPLOAD_FAILED` | 502 | Underlying `FileManager.uploadFile` returned `success: false` |
131
+
132
+ ## Subpath exports
133
+
134
+ | Import | Contents |
135
+ |---|---|
136
+ | `hazo_images` | Shared types + error classes (isomorphic, no Node deps) |
137
+ | `hazo_images/server` | `processImage`, `uploadProcessedImage` (Node.js only) |
138
+ | `hazo_images/ui` | Reserved for `@1.1+` UI components (empty in `@1.0`) |
139
+
140
+ ## Supported formats
141
+
142
+ Passes through whatever Sharp supports: JPEG, PNG, WebP, AVIF, TIFF, GIF (static), SVG (rasterized). HEIC requires the optional `sharp-heic` package installed separately.
143
+
144
+ ## Options reference
145
+
146
+ ```ts
147
+ interface ProcessImageOptions {
148
+ stripExif?: boolean; // default: true — strip all EXIF metadata
149
+ autoRotate?: boolean; // default: true — apply EXIF orientation, then strip tag
150
+ maxDimension?: number; // default: 4096 — longest side; 0 = disable resize
151
+ webp?: boolean; // default: false — transcode to WebP
152
+ thumbnails?: number[]; // default: [] — pixel sizes for longest side
153
+ }
154
+ ```
155
+
156
+ ## Roadmap
157
+
158
+ | Version | Scope |
159
+ |---|---|
160
+ | `@1.0` | Server pipeline: `processImage` + `uploadProcessedImage` |
161
+ | `@1.1+` | UI: redesigned AI-edit component, crop/rotate/filter primitives |
162
+ | `@1.2+` | Face detection via `face-api-node` (speculative, non-committed) |
163
+
164
+ ## License
165
+
166
+ MIT
@@ -0,0 +1,83 @@
1
+ # hazo_images Setup Checklist
2
+
3
+ ## 1. Install the package
4
+
5
+ ```bash
6
+ npm install hazo_images
7
+ ```
8
+
9
+ ## 1b. Install hazo_core (required peer dep)
10
+
11
+ `hazo_core` provides the `HazoError` base class, correlation-ID
12
+ propagation, the structured logger factory, and the INI config loader
13
+ used by `hazo_images`. It is a required peer dep — install it once at
14
+ the consuming app's root.
15
+
16
+ ```bash
17
+ npm install hazo_core
18
+ ```
19
+
20
+ ## 2. Install Sharp (required for image processing)
21
+
22
+ Sharp is an optional peer dependency. It is required whenever `processImage` or `uploadProcessedImage` is called. If Sharp is not installed, an error is thrown at call time (not at import time).
23
+
24
+ ```bash
25
+ npm install sharp
26
+ ```
27
+
28
+ Sharp installs platform-specific native bindings (~10 MB). On some CI systems you may need `npm install --ignore-scripts sharp` followed by a rebuild step.
29
+
30
+ ## 3. Install hazo_files (required for uploadProcessedImage)
31
+
32
+ Only needed if using the `uploadProcessedImage` helper. `processImage` works standalone without `hazo_files`.
33
+
34
+ ```bash
35
+ npm install hazo_files@^2.1.1
36
+ ```
37
+
38
+ ## 4. Server-only imports
39
+
40
+ Always import from `hazo_images/server` in server-side code only. Do NOT import `hazo_images/server` in client-bundled code (components, browser entry points). It has Node.js dependencies.
41
+
42
+ ```ts
43
+ // In API routes, server components, or Node.js scripts:
44
+ import { processImage, uploadProcessedImage } from 'hazo_images/server';
45
+
46
+ // Safe anywhere (types + errors, no Node deps):
47
+ import { ImageProcessingError, UnsupportedFormatError } from 'hazo_images';
48
+ ```
49
+
50
+ ## 5. Next.js apps — transpilePackages
51
+
52
+ Add `hazo_images` to `transpilePackages` in `next.config.js`:
53
+
54
+ ```js
55
+ const nextConfig = {
56
+ transpilePackages: ['hazo_images'],
57
+ };
58
+ module.exports = nextConfig;
59
+ ```
60
+
61
+ ## 6. (Optional) Drop in `hazo_images_config.ini`
62
+
63
+ The package ships `config/hazo_images_config.ini.sample` with sections
64
+ for `[general]`, `[log.overrides]`, and `[processing]` defaults. Copy
65
+ to `<consuming-app>/config/hazo_images_config.ini` (or the package root)
66
+ and override what you need. Any unset key falls back to the
67
+ `processImage` defaults. Values may also be overridden at runtime via
68
+ `HAZO_IMAGES_<SECTION>_<KEY>` env vars.
69
+
70
+ Per `D-020`, a per-environment overlay file at
71
+ `config/hazo_images_config.<HAZO_ENV>.ini` is layered on top of the
72
+ base file when `HAZO_ENV` is set.
73
+
74
+ ## 7. Verify
75
+
76
+ ```ts
77
+ import { processImage } from 'hazo_images/server';
78
+
79
+ const buffer = Buffer.from('...'); // a valid JPEG buffer
80
+ const result = await processImage(buffer, { thumbnails: [256] });
81
+ console.log(result.metadata); // { width, height, format }
82
+ console.log(result.thumbnails.length); // 1
83
+ ```
@@ -0,0 +1,53 @@
1
+ ; hazo_images configuration
2
+ ;
3
+ ; Copy to `hazo_images_config.ini` and customise. Loaded by getImagesConfig()
4
+ ; via hazo_core's loadConfig() on first access.
5
+ ;
6
+ ; Per hazo workspace standard S7, values may also be overridden via env vars:
7
+ ; HAZO_IMAGES_<SECTION>_<KEY>=value
8
+ ; e.g. HAZO_IMAGES_PROCESSING_MAX_DIMENSION=2048
9
+ ;
10
+ ; Per D-020, a per-environment overlay file at
11
+ ; config/hazo_images_config.<HAZO_ENV>.ini
12
+ ; is layered on top of this base file when HAZO_ENV is set.
13
+
14
+ ; ============================================================================
15
+ ; [general] - Workspace-standard general configuration
16
+ ; ============================================================================
17
+ [general]
18
+
19
+ ; Deployment environment label. Falls back to NODE_ENV, then 'development'.
20
+ ; env = production
21
+
22
+ ; ============================================================================
23
+ ; [log.overrides] - Per-namespace log level overrides (workspace standard)
24
+ ; Format: <namespace> = trace|debug|info|warn|error
25
+ ; ============================================================================
26
+ [log.overrides]
27
+
28
+ ; hazo_images = info
29
+ ; hazo_images/processing = warn
30
+
31
+ ; ============================================================================
32
+ ; [processing] - Default processImage option overrides
33
+ ; Each key here becomes the implicit default if the caller does not pass
34
+ ; an explicit value to processImage(buffer, opts).
35
+ ; ============================================================================
36
+ [processing]
37
+
38
+ ; Strip EXIF metadata from output buffers. true | false (default: true)
39
+ ; strip_exif = true
40
+
41
+ ; Auto-rotate using EXIF Orientation tag. true | false (default: true)
42
+ ; auto_rotate = true
43
+
44
+ ; Resize to fit within max_dimension × max_dimension. 0 disables resize.
45
+ ; Default: 4096
46
+ ; max_dimension = 4096
47
+
48
+ ; Re-encode to WebP. true | false (default: false)
49
+ ; webp = false
50
+
51
+ ; Comma-separated thumbnail sizes in pixels. Empty = no thumbnails.
52
+ ; Default: (none)
53
+ ; thumbnails = 256, 512, 1024
@@ -0,0 +1,79 @@
1
+ import { HazoExternalError, HazoConfigError, HazoValidationError } from 'hazo_core';
2
+
3
+ interface ProcessImageOptions {
4
+ stripExif?: boolean;
5
+ autoRotate?: boolean;
6
+ maxDimension?: number;
7
+ webp?: boolean;
8
+ thumbnails?: number[];
9
+ }
10
+ interface ProcessImageResult {
11
+ buffer: Buffer;
12
+ thumbnails: {
13
+ size: number;
14
+ buffer: Buffer;
15
+ format: string;
16
+ }[];
17
+ metadata: {
18
+ width: number;
19
+ height: number;
20
+ format: string;
21
+ };
22
+ }
23
+ interface UploadProcessedImageResult {
24
+ main: any;
25
+ thumbs: Record<number, any>;
26
+ }
27
+
28
+ /**
29
+ * hazo_images error classes.
30
+ *
31
+ * Per Wave 2 / D-008: all errors extend a HazoError subclass from hazo_core.
32
+ * Code namespace: HAZO_IMAGES_*.
33
+ *
34
+ * The legacy class names (`ImageProcessingError`, `UnsupportedFormatError`)
35
+ * are preserved for backward compatibility — existing test-app scenarios
36
+ * and the @1.0 public API still discriminate by `errorType`/`error.name`.
37
+ */
38
+
39
+ /**
40
+ * Sharp pipeline failure (encode/decode/resize error from the image library).
41
+ * Maps to HazoExternalError because the underlying failure originates outside
42
+ * our code path (Sharp / libvips). HTTP status 502, retryable=true.
43
+ *
44
+ * Code: HAZO_IMAGES_PROCESSING_FAILED
45
+ */
46
+ declare class ImageProcessingError extends HazoExternalError {
47
+ readonly originalError: Error;
48
+ constructor(originalError: Error, message?: string);
49
+ }
50
+ /**
51
+ * Caller passed a buffer Sharp couldn't decode (PDF, corrupt JPEG, etc).
52
+ * Maps to HazoValidationError because it's a bad-input problem on the
53
+ * caller's side. HTTP status 400.
54
+ *
55
+ * Code: HAZO_IMAGES_UNSUPPORTED_FORMAT
56
+ */
57
+ declare class UnsupportedFormatError extends HazoValidationError {
58
+ constructor(message?: string);
59
+ }
60
+ /**
61
+ * Optional peer `sharp` is not installed in the consuming app. Maps to
62
+ * HazoConfigError because the install/setup is misconfigured; there is no
63
+ * HTTP status — startup-time concern. Code: HAZO_IMAGES_CONFIG_SHARP_MISSING.
64
+ */
65
+ declare class SharpMissingError extends HazoConfigError {
66
+ constructor();
67
+ }
68
+ /**
69
+ * Underlying FileManager (hazo_files) rejected a buffer upload during
70
+ * uploadProcessedImage. HazoExternalError because the failure is in the
71
+ * downstream storage adapter, not in our code.
72
+ *
73
+ * Code: HAZO_IMAGES_UPLOAD_FAILED
74
+ */
75
+ declare class ImageUploadError extends HazoExternalError {
76
+ constructor(message: string, context?: Record<string, unknown>);
77
+ }
78
+
79
+ export { ImageProcessingError, ImageUploadError, type ProcessImageOptions, type ProcessImageResult, SharpMissingError, UnsupportedFormatError, type UploadProcessedImageResult };
package/dist/index.js ADDED
@@ -0,0 +1,53 @@
1
+ // src/errors.ts
2
+ import {
3
+ HazoConfigError,
4
+ HazoExternalError,
5
+ HazoValidationError
6
+ } from "hazo_core";
7
+ var ImageProcessingError = class extends HazoExternalError {
8
+ originalError;
9
+ constructor(originalError, message) {
10
+ super({
11
+ code: "HAZO_IMAGES_PROCESSING_FAILED",
12
+ pkg: "hazo_images",
13
+ message: message ?? originalError.message,
14
+ cause: originalError,
15
+ context: { originalErrorName: originalError.name }
16
+ });
17
+ this.originalError = originalError;
18
+ }
19
+ };
20
+ var UnsupportedFormatError = class extends HazoValidationError {
21
+ constructor(message = "Buffer is not a recognized image format") {
22
+ super({
23
+ code: "HAZO_IMAGES_UNSUPPORTED_FORMAT",
24
+ pkg: "hazo_images",
25
+ message
26
+ });
27
+ }
28
+ };
29
+ var SharpMissingError = class extends HazoConfigError {
30
+ constructor() {
31
+ super({
32
+ code: "HAZO_IMAGES_CONFIG_SHARP_MISSING",
33
+ pkg: "hazo_images",
34
+ message: "[hazo_images] Sharp is not installed. Run: npm install sharp\nSharp is an optional peer dependency required for image processing."
35
+ });
36
+ }
37
+ };
38
+ var ImageUploadError = class extends HazoExternalError {
39
+ constructor(message, context) {
40
+ super({
41
+ code: "HAZO_IMAGES_UPLOAD_FAILED",
42
+ pkg: "hazo_images",
43
+ message,
44
+ context
45
+ });
46
+ }
47
+ };
48
+ export {
49
+ ImageProcessingError,
50
+ ImageUploadError,
51
+ SharpMissingError,
52
+ UnsupportedFormatError
53
+ };
@@ -0,0 +1,93 @@
1
+ import { HazoExternalError, HazoConfigError, HazoValidationError } from 'hazo_core';
2
+
3
+ interface ProcessImageOptions {
4
+ stripExif?: boolean;
5
+ autoRotate?: boolean;
6
+ maxDimension?: number;
7
+ webp?: boolean;
8
+ thumbnails?: number[];
9
+ }
10
+ interface ProcessImageResult {
11
+ buffer: Buffer;
12
+ thumbnails: {
13
+ size: number;
14
+ buffer: Buffer;
15
+ format: string;
16
+ }[];
17
+ metadata: {
18
+ width: number;
19
+ height: number;
20
+ format: string;
21
+ };
22
+ }
23
+ interface UploadProcessedImageResult {
24
+ main: any;
25
+ thumbs: Record<number, any>;
26
+ }
27
+
28
+ declare function processImage(buffer: Buffer, opts?: ProcessImageOptions): Promise<ProcessImageResult>;
29
+
30
+ interface FileManager {
31
+ uploadFile(source: Buffer | string, remotePath: string, options?: Record<string, unknown>): Promise<{
32
+ success: boolean;
33
+ data?: unknown;
34
+ error?: string;
35
+ }>;
36
+ }
37
+ interface UploadOptions {
38
+ actor_id?: string;
39
+ }
40
+ declare function uploadProcessedImage(fm: FileManager, buffer: Buffer, virtualPath: string, opts?: ProcessImageOptions & UploadOptions): Promise<UploadProcessedImageResult>;
41
+
42
+ /**
43
+ * hazo_images error classes.
44
+ *
45
+ * Per Wave 2 / D-008: all errors extend a HazoError subclass from hazo_core.
46
+ * Code namespace: HAZO_IMAGES_*.
47
+ *
48
+ * The legacy class names (`ImageProcessingError`, `UnsupportedFormatError`)
49
+ * are preserved for backward compatibility — existing test-app scenarios
50
+ * and the @1.0 public API still discriminate by `errorType`/`error.name`.
51
+ */
52
+
53
+ /**
54
+ * Sharp pipeline failure (encode/decode/resize error from the image library).
55
+ * Maps to HazoExternalError because the underlying failure originates outside
56
+ * our code path (Sharp / libvips). HTTP status 502, retryable=true.
57
+ *
58
+ * Code: HAZO_IMAGES_PROCESSING_FAILED
59
+ */
60
+ declare class ImageProcessingError extends HazoExternalError {
61
+ readonly originalError: Error;
62
+ constructor(originalError: Error, message?: string);
63
+ }
64
+ /**
65
+ * Caller passed a buffer Sharp couldn't decode (PDF, corrupt JPEG, etc).
66
+ * Maps to HazoValidationError because it's a bad-input problem on the
67
+ * caller's side. HTTP status 400.
68
+ *
69
+ * Code: HAZO_IMAGES_UNSUPPORTED_FORMAT
70
+ */
71
+ declare class UnsupportedFormatError extends HazoValidationError {
72
+ constructor(message?: string);
73
+ }
74
+ /**
75
+ * Optional peer `sharp` is not installed in the consuming app. Maps to
76
+ * HazoConfigError because the install/setup is misconfigured; there is no
77
+ * HTTP status — startup-time concern. Code: HAZO_IMAGES_CONFIG_SHARP_MISSING.
78
+ */
79
+ declare class SharpMissingError extends HazoConfigError {
80
+ constructor();
81
+ }
82
+ /**
83
+ * Underlying FileManager (hazo_files) rejected a buffer upload during
84
+ * uploadProcessedImage. HazoExternalError because the failure is in the
85
+ * downstream storage adapter, not in our code.
86
+ *
87
+ * Code: HAZO_IMAGES_UPLOAD_FAILED
88
+ */
89
+ declare class ImageUploadError extends HazoExternalError {
90
+ constructor(message: string, context?: Record<string, unknown>);
91
+ }
92
+
93
+ export { ImageProcessingError, ImageUploadError, type ProcessImageOptions, type ProcessImageResult, SharpMissingError, UnsupportedFormatError, type UploadProcessedImageResult, processImage, uploadProcessedImage };
@@ -0,0 +1,227 @@
1
+ // src/errors.ts
2
+ import {
3
+ HazoConfigError,
4
+ HazoExternalError,
5
+ HazoValidationError
6
+ } from "hazo_core";
7
+ var ImageProcessingError = class extends HazoExternalError {
8
+ originalError;
9
+ constructor(originalError, message) {
10
+ super({
11
+ code: "HAZO_IMAGES_PROCESSING_FAILED",
12
+ pkg: "hazo_images",
13
+ message: message ?? originalError.message,
14
+ cause: originalError,
15
+ context: { originalErrorName: originalError.name }
16
+ });
17
+ this.originalError = originalError;
18
+ }
19
+ };
20
+ var UnsupportedFormatError = class extends HazoValidationError {
21
+ constructor(message = "Buffer is not a recognized image format") {
22
+ super({
23
+ code: "HAZO_IMAGES_UNSUPPORTED_FORMAT",
24
+ pkg: "hazo_images",
25
+ message
26
+ });
27
+ }
28
+ };
29
+ var SharpMissingError = class extends HazoConfigError {
30
+ constructor() {
31
+ super({
32
+ code: "HAZO_IMAGES_CONFIG_SHARP_MISSING",
33
+ pkg: "hazo_images",
34
+ message: "[hazo_images] Sharp is not installed. Run: npm install sharp\nSharp is an optional peer dependency required for image processing."
35
+ });
36
+ }
37
+ };
38
+ var ImageUploadError = class extends HazoExternalError {
39
+ constructor(message, context) {
40
+ super({
41
+ code: "HAZO_IMAGES_UPLOAD_FAILED",
42
+ pkg: "hazo_images",
43
+ message,
44
+ context
45
+ });
46
+ }
47
+ };
48
+
49
+ // src/utils/logger.ts
50
+ import { createLogger } from "hazo_core";
51
+ var console_logger = {
52
+ info: (m, d) => d ? console.log(`[hazo_images] ${m}`, d) : console.log(`[hazo_images] ${m}`),
53
+ debug: (m, d) => d ? console.debug(`[hazo_images] ${m}`, d) : console.debug(`[hazo_images] ${m}`),
54
+ warn: (m, d) => d ? console.warn(`[hazo_images] ${m}`, d) : console.warn(`[hazo_images] ${m}`),
55
+ error: (m, d) => d ? console.error(`[hazo_images] ${m}`, d) : console.error(`[hazo_images] ${m}`)
56
+ };
57
+ function build_default_logger() {
58
+ try {
59
+ return createLogger("hazo_images");
60
+ } catch {
61
+ return console_logger;
62
+ }
63
+ }
64
+ var current_logger = null;
65
+ function get_logger() {
66
+ if (!current_logger) {
67
+ current_logger = build_default_logger();
68
+ }
69
+ return current_logger;
70
+ }
71
+
72
+ // src/server/process-image.ts
73
+ var sharpPromise = null;
74
+ async function getSharp() {
75
+ if (!sharpPromise) {
76
+ sharpPromise = import("sharp").then((m) => m.default ?? m).catch(() => {
77
+ sharpPromise = null;
78
+ throw new SharpMissingError();
79
+ });
80
+ }
81
+ return sharpPromise;
82
+ }
83
+ async function processImage(buffer, opts = {}) {
84
+ const {
85
+ stripExif = true,
86
+ autoRotate = true,
87
+ maxDimension = 4096,
88
+ webp = false,
89
+ thumbnails = []
90
+ } = opts;
91
+ const log = get_logger();
92
+ const sharpFn = await getSharp();
93
+ log.debug("process_image.started", {
94
+ inputBytes: buffer.byteLength,
95
+ webp,
96
+ maxDimension,
97
+ thumbnailCount: thumbnails.length
98
+ });
99
+ let inputMetadata;
100
+ try {
101
+ inputMetadata = await sharpFn(buffer).metadata();
102
+ } catch (err) {
103
+ throw new UnsupportedFormatError();
104
+ }
105
+ if (!inputMetadata.format) {
106
+ throw new UnsupportedFormatError();
107
+ }
108
+ let pipeline = sharpFn(buffer);
109
+ if (autoRotate) {
110
+ pipeline = pipeline.autoOrient();
111
+ }
112
+ if (!stripExif) {
113
+ pipeline = pipeline.withMetadata();
114
+ }
115
+ if (maxDimension > 0) {
116
+ pipeline = pipeline.resize({
117
+ width: maxDimension,
118
+ height: maxDimension,
119
+ fit: "inside",
120
+ withoutEnlargement: true
121
+ });
122
+ }
123
+ let mainBuffer;
124
+ let outputMetadata;
125
+ try {
126
+ if (webp) {
127
+ const { data, info } = await pipeline.webp().toBuffer({ resolveWithObject: true });
128
+ mainBuffer = data;
129
+ outputMetadata = { width: info.width, height: info.height, format: "webp" };
130
+ } else {
131
+ const { data, info } = await pipeline.toBuffer({ resolveWithObject: true });
132
+ mainBuffer = data;
133
+ outputMetadata = { width: info.width, height: info.height, format: info.format };
134
+ }
135
+ } catch (err) {
136
+ throw new ImageProcessingError(err);
137
+ }
138
+ const thumbResults = [];
139
+ for (const size of thumbnails) {
140
+ try {
141
+ const thumbPipeline = sharpFn(mainBuffer).resize({
142
+ width: size,
143
+ height: size,
144
+ fit: "inside",
145
+ withoutEnlargement: true
146
+ });
147
+ let thumbBuffer;
148
+ let thumbFormat;
149
+ if (webp) {
150
+ const { data } = await thumbPipeline.webp().toBuffer({ resolveWithObject: true });
151
+ thumbBuffer = data;
152
+ thumbFormat = "webp";
153
+ } else {
154
+ const { data, info } = await thumbPipeline.toBuffer({ resolveWithObject: true });
155
+ thumbBuffer = data;
156
+ thumbFormat = info.format;
157
+ }
158
+ thumbResults.push({ size, buffer: thumbBuffer, format: thumbFormat });
159
+ } catch (err) {
160
+ throw new ImageProcessingError(
161
+ err,
162
+ `Thumbnail generation failed for size ${size}`
163
+ );
164
+ }
165
+ }
166
+ log.debug("process_image.completed", {
167
+ outputBytes: mainBuffer.byteLength,
168
+ outputFormat: outputMetadata.format,
169
+ outputWidth: outputMetadata.width,
170
+ outputHeight: outputMetadata.height,
171
+ thumbnailsGenerated: thumbResults.length
172
+ });
173
+ return {
174
+ buffer: mainBuffer,
175
+ thumbnails: thumbResults,
176
+ metadata: outputMetadata
177
+ };
178
+ }
179
+
180
+ // src/server/upload-processed-image.ts
181
+ import path from "path";
182
+ async function uploadProcessedImage(fm, buffer, virtualPath, opts) {
183
+ const { actor_id, ...processOpts } = opts ?? {};
184
+ const log = get_logger();
185
+ const result = await processImage(buffer, processOpts);
186
+ const uploadOpts = actor_id !== void 0 ? { actor_id } : void 0;
187
+ const mainResult = await fm.uploadFile(result.buffer, virtualPath, uploadOpts);
188
+ if (!mainResult.success) {
189
+ throw new ImageUploadError(
190
+ `Failed to upload main image: ${mainResult.error}`,
191
+ { virtualPath, fileManagerError: mainResult.error }
192
+ );
193
+ }
194
+ const thumbs = {};
195
+ const ext = path.extname(virtualPath);
196
+ const basename = path.basename(virtualPath, ext);
197
+ const dir = path.dirname(virtualPath);
198
+ const prefix = dir === "/" ? "" : dir;
199
+ for (const thumb of result.thumbnails) {
200
+ const thumbExt = thumb.format === "webp" ? ".webp" : ext;
201
+ const thumbPath = `${prefix}/${basename}__thumb_${thumb.size}${thumbExt}`;
202
+ const thumbResult = await fm.uploadFile(thumb.buffer, thumbPath, uploadOpts);
203
+ if (!thumbResult.success) {
204
+ throw new ImageUploadError(
205
+ `Failed to upload thumbnail ${thumb.size}px: ${thumbResult.error}`,
206
+ { virtualPath: thumbPath, thumbSize: thumb.size, fileManagerError: thumbResult.error }
207
+ );
208
+ }
209
+ thumbs[thumb.size] = thumbResult.data;
210
+ }
211
+ log.debug("upload_processed_image.completed", {
212
+ virtualPath,
213
+ thumbnailCount: result.thumbnails.length
214
+ });
215
+ return {
216
+ main: mainResult.data,
217
+ thumbs
218
+ };
219
+ }
220
+ export {
221
+ ImageProcessingError,
222
+ ImageUploadError,
223
+ SharpMissingError,
224
+ UnsupportedFormatError,
225
+ processImage,
226
+ uploadProcessedImage
227
+ };
@@ -0,0 +1,2 @@
1
+
2
+ export { }
File without changes
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "hazo_images",
3
+ "version": "1.1.0",
4
+ "description": "Image processing pipeline for the hazo ecosystem — Sharp wrapper, thumbnail generation, EXIF handling, and hazo_files integration helper",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "default": "./dist/index.js"
15
+ },
16
+ "./server": {
17
+ "types": "./dist/server/index.d.ts",
18
+ "import": "./dist/server/index.js",
19
+ "default": "./dist/server/index.js"
20
+ },
21
+ "./ui": {
22
+ "types": "./dist/ui/index.d.ts",
23
+ "import": "./dist/ui/index.js",
24
+ "default": "./dist/ui/index.js"
25
+ }
26
+ },
27
+ "files": [
28
+ "dist",
29
+ "config",
30
+ "README.md",
31
+ "SETUP_CHECKLIST.md",
32
+ "CHANGE_LOG.md"
33
+ ],
34
+ "scripts": {
35
+ "build": "tsup",
36
+ "dev": "tsup --watch",
37
+ "typecheck": "tsc --noEmit",
38
+ "lint": "tsc --noEmit"
39
+ },
40
+ "dependencies": {
41
+ "ini": "^4.1.0",
42
+ "zod": "^3.23.8"
43
+ },
44
+ "peerDependencies": {
45
+ "hazo_core": "^1.0.0",
46
+ "hazo_files": "^3.0.0",
47
+ "react": "^18.0.0 || ^19.0.0",
48
+ "react-dom": "^18.0.0 || ^19.0.0",
49
+ "sharp": "^0.33.0"
50
+ },
51
+ "peerDependenciesMeta": {
52
+ "hazo_files": {
53
+ "optional": true
54
+ },
55
+ "react": {
56
+ "optional": true
57
+ },
58
+ "react-dom": {
59
+ "optional": true
60
+ },
61
+ "sharp": {
62
+ "optional": true
63
+ }
64
+ },
65
+ "devDependencies": {
66
+ "@types/node": "^22.10.0",
67
+ "@types/react": "^19.0.0",
68
+ "hazo_core": "^1.0.0",
69
+ "tsup": "^8.0.0",
70
+ "typescript": "^5.7.2"
71
+ },
72
+ "sideEffects": false
73
+ }