hazo_images 1.8.0 → 1.9.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 CHANGED
@@ -1,5 +1,15 @@
1
1
  # hazo_images Change Log
2
2
 
3
+ ## 1.9.0 — 2026-07-21
4
+
5
+ ### Added
6
+
7
+ - **`bboxIoU(a, b)`** — new root export (`hazo_images`, isomorphic). Computes Intersection-over-Union for two normalized (0..1) axis-aligned bounding boxes. Returns `0` when either input is `null`/`undefined`/malformed instead of throwing — callers commonly iterate user-tagged boxes that may legitimately be missing a region. New export: `BBox` type. Ported from a downstream app's photo face-tag dedup logic; pure, no dependencies.
8
+ - **`canDecodeImage(file)`** — new `hazo_images/ui` export (client-only). Probes whether the current browser can decode a given `File` by loading it into an `Image` and resolving `true`/`false` on `load`/`error` (revoking the created object URL either way). Ported from a downstream app's crop-dialog gating logic (e.g. skip the cropper and fall back to direct upload for formats like HEIC that some browsers can't render). Not a React component — plain function, no `hazo_ui`/React import.
9
+ - Demo: `test-app /bbox-and-decode` page exercising both utilities (draggable-value box overlay for `bboxIoU`, file-picker probe for `canDecodeImage`).
10
+
11
+ ---
12
+
3
13
  ## 1.8.0 — 2026-07-11
4
14
 
5
15
  ### Added
package/README.md CHANGED
@@ -91,6 +91,39 @@ const result = await processImage(buffer, {
91
91
  // thumbnail extension in uploadProcessedImage: __thumb_256.webp
92
92
  ```
93
93
 
94
+ ### bboxIoU — bounding-box overlap (isomorphic)
95
+
96
+ ```ts
97
+ import { bboxIoU } from 'hazo_images';
98
+ import type { BBox } from 'hazo_images';
99
+
100
+ const a: BBox = { x: 0.1, y: 0.1, w: 0.4, h: 0.4 };
101
+ const b: BBox = { x: 0.3, y: 0.3, w: 0.4, h: 0.4 };
102
+
103
+ bboxIoU(a, b); // 0..1, where 1 = perfect overlap
104
+ bboxIoU(a, null); // 0 — null/undefined/malformed input never throws
105
+ ```
106
+
107
+ Coordinates are normalized `0..1` relative to the containing image. Useful
108
+ for deduping or matching user-tagged regions (e.g. face-tag boxes) against
109
+ detector output.
110
+
111
+ ### canDecodeImage — browser decode probe (client-only)
112
+
113
+ ```ts
114
+ import { canDecodeImage } from 'hazo_images/ui';
115
+
116
+ const ok = await canDecodeImage(file); // File → Promise<boolean>
117
+ if (!ok) {
118
+ // e.g. HEIC on a non-Safari browser — skip the crop dialog and
119
+ // fall back to direct upload instead.
120
+ }
121
+ ```
122
+
123
+ Loads the file into an `Image` and resolves `true`/`false` based on
124
+ `load`/`error`; the created object URL is revoked either way. Browser-only —
125
+ relies on global `Image`/`URL`/`File`, so it must run in a client component.
126
+
94
127
  ## Error Types
95
128
 
96
129
  All errors extend `HazoError` subclasses from `hazo_core` (Wave 2). The
@@ -136,9 +169,9 @@ try {
136
169
 
137
170
  | Import | Contents |
138
171
  |---|---|
139
- | `hazo_images` | Shared types + error classes (isomorphic, no Node deps) |
172
+ | `hazo_images` | Shared types + error classes + `bboxIoU` (isomorphic, no Node deps) |
140
173
  | `hazo_images/server` | `processImage`, `uploadProcessedImage`, `createImageProcessHandler` (Node.js only) |
141
- | `hazo_images/ui` | `ImageUploader`, `ImageViewer`, `ImageEditorDialog`, `ImageAnnotator` + control-schema helpers (React, client-only) |
174
+ | `hazo_images/ui` | `ImageUploader`, `ImageViewer`, `ImageEditorDialog`, `ImageAnnotator`, `canDecodeImage` + control-schema helpers (client-only; React components require `hazo_ui`, `canDecodeImage` does not) |
142
175
  | `hazo_images/runware` | `createRunwareClient`, `assemblePrompts`, 4 error classes (Node.js only) |
143
176
 
144
177
  ## Supported formats
package/dist/index.d.ts CHANGED
@@ -152,4 +152,25 @@ declare class ImageUploadError extends HazoExternalError {
152
152
  constructor(message: string, context?: Record<string, unknown>);
153
153
  }
154
154
 
155
- export { ImageProcessingError, ImageUploadError, type ProcessImageOptions, type ProcessImageResult, SharpMissingError, UnsupportedFormatError, type UploadProcessedImageResult };
155
+ /**
156
+ * Normalized axis-aligned bounding box: coordinates and dimensions in the
157
+ * 0..1 range relative to the containing image.
158
+ */
159
+ type BBox = {
160
+ x: number;
161
+ y: number;
162
+ w: number;
163
+ h: number;
164
+ };
165
+ /**
166
+ * Compute Intersection over Union for two axis-aligned bounding boxes.
167
+ * All coordinates are normalized 0..1.
168
+ *
169
+ * Returns a value in [0..1] where 1 means perfect overlap. Returns 0 when
170
+ * either input is null/undefined or otherwise malformed — callers commonly
171
+ * iterate user-tagged bboxes that may legitimately be null (e.g. a tag
172
+ * created without a drawn region carries no bbox).
173
+ */
174
+ declare function bboxIoU(a: BBox | null | undefined, b: BBox | null | undefined): number;
175
+
176
+ export { type BBox, ImageProcessingError, ImageUploadError, type ProcessImageOptions, type ProcessImageResult, SharpMissingError, UnsupportedFormatError, type UploadProcessedImageResult, bboxIoU };
package/dist/index.js CHANGED
@@ -45,9 +45,36 @@ var ImageUploadError = class extends HazoExternalError {
45
45
  });
46
46
  }
47
47
  };
48
+
49
+ // src/bbox-iou.ts
50
+ function isValidBBox(b) {
51
+ if (!b || typeof b !== "object") return false;
52
+ const r = b;
53
+ return typeof r.x === "number" && typeof r.y === "number" && typeof r.w === "number" && typeof r.h === "number";
54
+ }
55
+ function bboxIoU(a, b) {
56
+ if (!isValidBBox(a) || !isValidBBox(b)) return 0;
57
+ const ax2 = a.x + a.w;
58
+ const ay2 = a.y + a.h;
59
+ const bx2 = b.x + b.w;
60
+ const by2 = b.y + b.h;
61
+ const interX1 = Math.max(a.x, b.x);
62
+ const interY1 = Math.max(a.y, b.y);
63
+ const interX2 = Math.min(ax2, bx2);
64
+ const interY2 = Math.min(ay2, by2);
65
+ const interW = interX2 - interX1;
66
+ const interH = interY2 - interY1;
67
+ if (interW <= 0 || interH <= 0) return 0;
68
+ const intersection = interW * interH;
69
+ const areaA = a.w * a.h;
70
+ const areaB = b.w * b.h;
71
+ const union = areaA + areaB - intersection;
72
+ return union <= 0 ? 0 : intersection / union;
73
+ }
48
74
  export {
49
75
  ImageProcessingError,
50
76
  ImageUploadError,
51
77
  SharpMissingError,
52
- UnsupportedFormatError
78
+ UnsupportedFormatError,
79
+ bboxIoU
53
80
  };
@@ -393,4 +393,17 @@ declare function ResizeHandleOverlay({ box, onResize, onResizeEnd, containerWidt
393
393
 
394
394
  declare function cn(...parts: Array<string | false | null | undefined>): string;
395
395
 
396
- export { type AnnotatorShape, type AnnotatorTool, type ArrowShape, type CircleShape, DEFAULT_IMAGE_EDIT_CONTROLS, ImageAnnotator, type ImageAnnotatorProps, type ImageEditControls, type ImageEditTab, ImageEditorDialog, type ImageEditorDialogProps, type ImageProcessOptions, type ImageProcessResult, type ImageProcessThumbnail, ImageUploader, type ImageUploaderProps, ImageViewer, type ImageViewerProps, type InputOption, type PenShape, type ProcessFn, type ResizeBox, type ResizeHandle, ResizeHandleOverlay, type ResizeHandleOverlayProps, type TabElement, type ValidationError, type ValidationResult, addShape, clearShapes, cn, controlValuesToOptions, defaultControlValues, undoShape, validate_image_edit_controls };
396
+ /**
397
+ * Probes whether the current browser can decode a given image File.
398
+ *
399
+ * Resolves `true` if the browser loads the file successfully, `false` if
400
+ * it fires an error (e.g. HEIC on non-Safari). Callers use this to decide
401
+ * whether to open a crop/edit dialog or fall back to direct upload.
402
+ *
403
+ * Browser-only — relies on the global `Image`, `URL.createObjectURL`, and
404
+ * `File` APIs. Never call this from server code or Node.js tests without
405
+ * stubbing those globals first.
406
+ */
407
+ declare function canDecodeImage(file: File): Promise<boolean>;
408
+
409
+ export { type AnnotatorShape, type AnnotatorTool, type ArrowShape, type CircleShape, DEFAULT_IMAGE_EDIT_CONTROLS, ImageAnnotator, type ImageAnnotatorProps, type ImageEditControls, type ImageEditTab, ImageEditorDialog, type ImageEditorDialogProps, type ImageProcessOptions, type ImageProcessResult, type ImageProcessThumbnail, ImageUploader, type ImageUploaderProps, ImageViewer, type ImageViewerProps, type InputOption, type PenShape, type ProcessFn, type ResizeBox, type ResizeHandle, ResizeHandleOverlay, type ResizeHandleOverlayProps, type TabElement, type ValidationError, type ValidationResult, addShape, canDecodeImage, clearShapes, cn, controlValuesToOptions, defaultControlValues, undoShape, validate_image_edit_controls };
package/dist/ui/index.js CHANGED
@@ -1894,6 +1894,23 @@ function ImageAnnotator({
1894
1894
  )
1895
1895
  ] }) });
1896
1896
  }
1897
+
1898
+ // src/ui/can-decode-image.ts
1899
+ function canDecodeImage(file) {
1900
+ return new Promise((resolve) => {
1901
+ const url = URL.createObjectURL(file);
1902
+ const img = new Image();
1903
+ img.onload = () => {
1904
+ URL.revokeObjectURL(url);
1905
+ resolve(true);
1906
+ };
1907
+ img.onerror = () => {
1908
+ URL.revokeObjectURL(url);
1909
+ resolve(false);
1910
+ };
1911
+ img.src = url;
1912
+ });
1913
+ }
1897
1914
  export {
1898
1915
  DEFAULT_IMAGE_EDIT_CONTROLS,
1899
1916
  ImageAnnotator,
@@ -1902,6 +1919,7 @@ export {
1902
1919
  ImageViewer,
1903
1920
  ResizeHandleOverlay,
1904
1921
  addShape,
1922
+ canDecodeImage,
1905
1923
  clearShapes,
1906
1924
  cn,
1907
1925
  controlValuesToOptions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_images",
3
- "version": "1.8.0",
3
+ "version": "1.9.0",
4
4
  "description": "Image processing pipeline for the hazo ecosystem — Sharp wrapper, thumbnail generation, EXIF handling, and hazo_files integration helper",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -51,15 +51,15 @@
51
51
  "zod": "^4.1.12"
52
52
  },
53
53
  "peerDependencies": {
54
- "hazo_core": "^1.2.1",
54
+ "hazo_core": "^1.3.0",
55
55
  "hazo_files": "^3.1.1",
56
- "hazo_theme": "^1.0.0",
57
- "hazo_ui": "^6.0.0",
56
+ "hazo_theme": "^1.0.1",
57
+ "hazo_ui": "^6.3.1",
58
58
  "konva": "^10.3.0",
59
59
  "lucide-react": "^0.553.0",
60
60
  "react": "^18.0.0 || ^19.0.0",
61
61
  "react-dom": "^18.0.0 || ^19.0.0",
62
- "react-konva": "^19.2.5",
62
+ "react-konva": "^18.2.16 || ^19.2.5",
63
63
  "sharp": "^0.33.0"
64
64
  },
65
65
  "peerDependenciesMeta": {
@@ -95,12 +95,12 @@
95
95
  "@types/jest": "^30.0.0",
96
96
  "@types/node": "^22.10.0",
97
97
  "@types/react": "^18.3.3",
98
- "hazo_core": "^1.2.1",
98
+ "hazo_core": "^1.3.0",
99
99
  "jest": "^30.2.0",
100
100
  "jest-environment-node": "^30.2.0",
101
101
  "konva": "^10.3.0",
102
102
  "lucide-react": "^0.553.0",
103
- "react-konva": "^19.2.5",
103
+ "react-konva": "^18.2.16",
104
104
  "ts-jest": "^29.4.5",
105
105
  "tsup": "^8.0.0",
106
106
  "typescript": "^5.7.2"