@cytario/web 2.1.4 → 2.1.6
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/README.md +20 -20
- package/app/.server/auth/README.md +8 -8
- package/app/.server/auth/authMiddleware.ts +9 -25
- package/app/.server/auth/exchangeAuthCode.ts +2 -6
- package/app/.server/auth/getS3Client.ts +3 -13
- package/app/.server/auth/getSessionCredentials.ts +6 -20
- package/app/.server/auth/getUserInfo.ts +2 -6
- package/app/.server/auth/keycloakAdmin/client.ts +2 -9
- package/app/.server/auth/keycloakAdmin/groups.ts +9 -26
- package/app/.server/auth/keycloakAdmin/users.ts +7 -23
- package/app/.server/auth/oauthState.ts +4 -13
- package/app/.server/auth/redirectIfAuthenticated.ts +1 -3
- package/app/.server/auth/refreshAuthTokens.ts +5 -19
- package/app/.server/auth/sessionMiddleware.ts +1 -4
- package/app/.server/auth/sessionStorage.ts +1 -4
- package/app/.server/auth/verifyIdToken.ts +1 -3
- package/app/.server/auth/wellKnownEndpoints.ts +1 -4
- package/app/.server/db/redis.ts +5 -1
- package/app/.server/logging.ts +1 -4
- package/app/.server/requestDurationMiddleware.ts +1 -4
- package/app/components/.client/ImageViewer/README.md +5 -5
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsController.tsx +7 -9
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerBrightfieldItem.tsx +1 -2
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItem.tsx +2 -5
- package/app/components/.client/ImageViewer/components/ChannelsController/ChannelsControllerItemList.tsx +7 -15
- package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorPicker.tsx +2 -9
- package/app/components/.client/ImageViewer/components/ChannelsController/ColorPicker/ColorSwatch.tsx +1 -4
- package/app/components/.client/ImageViewer/components/ChannelsController/DomainSlider.tsx +1 -3
- package/app/components/.client/ImageViewer/components/ChannelsController/Histogram.tsx +16 -18
- package/app/components/.client/ImageViewer/components/ChannelsController/HistogramChannel.tsx +2 -8
- package/app/components/.client/ImageViewer/components/ChannelsController/MinMaxSettings.tsx +6 -15
- package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarDragHandle.tsx +1 -5
- package/app/components/.client/ImageViewer/components/FeatureBar/FeatureBarToggle.tsx +1 -5
- package/app/components/.client/ImageViewer/components/FeatureBar/FeatureItem.tsx +1 -5
- package/app/components/.client/ImageViewer/components/FeatureBar/Presets.tsx +3 -11
- package/app/components/.client/ImageViewer/components/FeatureBar/useFeatureBar.tsx +16 -25
- package/app/components/.client/ImageViewer/components/Image/Channels/useChannelsLayer.ts +7 -18
- package/app/components/.client/ImageViewer/components/Image/ImageContainer.tsx +1 -1
- package/app/components/.client/ImageViewer/components/Image/ImagePanel.tsx +6 -26
- package/app/components/.client/ImageViewer/components/Image/ImagePreview.tsx +2 -9
- package/app/components/.client/ImageViewer/components/Image/Overlays/AdditivePolygonLayer.tsx +1 -5
- package/app/components/.client/ImageViewer/components/Image/Overlays/AdditiveScatterplotLayer.tsx +1 -5
- package/app/components/.client/ImageViewer/components/Image/Overlays/OverlaysLayer.tsx +6 -24
- package/app/components/.client/ImageViewer/components/Image/Overlays/markerUniforms.ts +2 -5
- package/app/components/.client/ImageViewer/components/Image/Overlays/useOverlaysLayer.tsx +7 -21
- package/app/components/.client/ImageViewer/components/Image/useInitializeChannels.ts +1 -7
- package/app/components/.client/ImageViewer/components/Image/useResizeObserver.ts +1 -1
- package/app/components/.client/ImageViewer/components/Magnifier.tsx +5 -13
- package/app/components/.client/ImageViewer/components/Measurements/ActiveViewStatePreview.tsx +3 -8
- package/app/components/.client/ImageViewer/components/Measurements/CursorTick.tsx +2 -7
- package/app/components/.client/ImageViewer/components/Measurements/Ruler.tsx +1 -8
- package/app/components/.client/ImageViewer/components/Measurements/SlideCarrier.tsx +3 -13
- package/app/components/.client/ImageViewer/components/Measurements/Tick.tsx +1 -1
- package/app/components/.client/ImageViewer/components/Measurements/calculateViewStateToFit.ts +1 -1
- package/app/components/.client/ImageViewer/components/Measurements/useMeasurements.ts +9 -28
- package/app/components/.client/ImageViewer/components/OverlaysController/AddOverlay.tsx +4 -13
- package/app/components/.client/ImageViewer/components/OverlaysController/OverlayPicker.modal.tsx +1 -6
- package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.Item.tsx +24 -54
- package/app/components/.client/ImageViewer/components/OverlaysController/OverlaysController.tsx +1 -3
- package/app/components/.client/ImageViewer/components/SplitViewToggle.tsx +1 -3
- package/app/components/.client/ImageViewer/components/ViewerHeader.tsx +1 -3
- package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.d.ts +9 -11
- package/app/components/.client/ImageViewer/state/decoders/decodeJPEG2000.js +11 -11
- package/app/components/.client/ImageViewer/state/decoders/decoder.worker.js +49 -49
- package/app/components/.client/ImageViewer/state/decoders/genericDecoder.ts +76 -81
- package/app/components/.client/ImageViewer/state/decoders/jp2k-decoder.ts +9 -9
- package/app/components/.client/ImageViewer/state/decoders/lzwDecoder.ts +9 -9
- package/app/components/.client/ImageViewer/state/loaders/loadBioformatsZarrWithCredentials.ts +10 -22
- package/app/components/.client/ImageViewer/state/store/ViewerStoreContext.tsx +4 -18
- package/app/components/.client/ImageViewer/state/store/createViewerStore.ts +110 -194
- package/app/components/.client/ImageViewer/state/store/getInitialChannelsState.ts +2 -6
- package/app/components/.client/ImageViewer/state/store/selectors.ts +9 -9
- package/app/components/.client/ImageViewer/state/store/types.ts +3 -12
- package/app/components/.client/ImageViewer/state/transport/CredentialedHTTPStore.ts +1 -5
- package/app/components/.client/ImageViewer/state/transport/SigV4TiffClient.ts +2 -9
- package/app/components/.client/ImageViewer/utils/getSelectionStats.ts +1 -4
- package/app/components/.client/ImageViewer/utils/handleImageViewerHover.ts +1 -1
- package/app/components/.client/ImageViewer/utils/mapChannelConfigsToState.ts +2 -4
- package/app/components/.client/ImageViewer/utils/useTilesLoading.ts +3 -3
- package/app/components/AppHeader.tsx +1 -4
- package/app/components/Breadcrumbs/Breadcrumbs.tsx +4 -13
- package/app/components/Breadcrumbs/getCrumbs.tsx +1 -1
- package/app/components/ClientOnly.tsx +1 -1
- package/app/components/Container.tsx +3 -15
- package/app/components/DataGrid/ConvertOverlay.modal.tsx +1 -6
- package/app/components/DataGrid/DataGrid.tsx +7 -27
- package/app/components/DataGrid/WktSvg.tsx +2 -4
- package/app/components/DataGrid/getParquetSchema.ts +1 -4
- package/app/components/DescriptionList.tsx +1 -3
- package/app/components/DirectoryView/ConnectionMenu.tsx +8 -22
- package/app/components/DirectoryView/DirectoryView.tsx +10 -46
- package/app/components/DirectoryView/DirectoryViewGrid.tsx +16 -49
- package/app/components/DirectoryView/DirectoryViewTableConnection.tsx +1 -4
- package/app/components/DirectoryView/DirectoryViewTableDirectory.tsx +2 -7
- package/app/components/DirectoryView/DirectoryViewTree.tsx +5 -21
- package/app/components/DirectoryView/FilterBar.tsx +9 -48
- package/app/components/DirectoryView/buildDirectoryTree.ts +6 -25
- package/app/components/DirectoryView/filterNodes.ts +4 -11
- package/app/components/DirectoryView/modals/Cyberduck.modal.tsx +6 -15
- package/app/components/DirectoryView/modals/FileInfo.modal.tsx +1 -5
- package/app/components/DirectoryView/useLayoutStore.ts +5 -25
- package/app/components/GlobalSearch/GlobalSearch.tsx +1 -4
- package/app/components/GlobalSearch/SearchBar.tsx +1 -7
- package/app/components/GlobalSearch/Suggestions.tsx +0 -1
- package/app/components/ImageViewer/state/formatRegistry.ts +5 -18
- package/app/components/LavaLoader.tsx +4 -11
- package/app/components/Layout/Footer.tsx +1 -4
- package/app/components/Pills/ScopePill.tsx +2 -8
- package/app/components/Table/ColumnFilterInput.tsx +5 -20
- package/app/components/Table/ColumnResizeHandle.tsx +1 -5
- package/app/components/Table/ColumnSortButton.tsx +1 -5
- package/app/components/Table/SelectionFooter.tsx +3 -5
- package/app/components/Table/Table.tsx +5 -21
- package/app/components/Table/TableBodyRow.tsx +19 -31
- package/app/components/Table/TableHeaderRow.tsx +7 -28
- package/app/components/Table/TableMenu.tsx +4 -20
- package/app/components/Table/state/createTableStore.ts +3 -9
- package/app/components/Table/state/useTableStore.ts +1 -3
- package/app/components/Table/types.ts +4 -10
- package/app/components/Table/useColumnFilters.ts +1 -4
- package/app/components/Table/useColumnVisibility.ts +4 -11
- package/app/components/Table/useColumnWidths.ts +1 -3
- package/app/components/Table/useTableSorting.ts +4 -12
- package/app/components/Tooltip/Tooltip.tsx +4 -17
- package/app/components/Tooltip/TooltipSpan.tsx +5 -28
- package/app/components/Tooltip/useCopyToClipboard.ts +1 -3
- package/app/components/Tooltip/useMiddleEllipsis.ts +2 -7
- package/app/components/Tooltip/useOverflowDetection.ts +2 -5
- package/app/components/UserMenu.tsx +2 -9
- package/app/entry.server.tsx +9 -19
- package/app/hooks/useSearchParam.ts +2 -4
- package/app/lib/bootstrapPluginsCore.ts +4 -9
- package/app/root.tsx +4 -15
- package/app/routes/admin/assertAdminScope.ts +1 -3
- package/app/routes/admin/assertGroupPathsInScope.ts +3 -11
- package/app/routes/admin/assertGroupsInScope.ts +3 -11
- package/app/routes/admin/assertUsersInScope.ts +2 -8
- package/app/routes/admin/bulkInvite/bulkInvite.action.ts +2 -13
- package/app/routes/admin/bulkInvite/bulkInvite.form.tsx +18 -35
- package/app/routes/admin/createGroup/createGroup.action.ts +3 -10
- package/app/routes/admin/createGroup/createGroup.form.tsx +2 -9
- package/app/routes/admin/createGroup/createGroup.modal.tsx +1 -5
- package/app/routes/admin/inviteUser/inviteUser.action.ts +1 -4
- package/app/routes/admin/inviteUser/inviteUser.form.tsx +2 -8
- package/app/routes/admin/inviteUser/inviteUser.loader.ts +1 -4
- package/app/routes/admin/inviteUser/inviteUser.modal.tsx +3 -16
- package/app/routes/admin/updateUser/updateUser.form.tsx +4 -15
- package/app/routes/admin/updateUser/updateUser.modal.tsx +5 -23
- package/app/routes/admin/updateUser/userDetail.action.ts +2 -10
- package/app/routes/admin/users/BulkActions.tsx +15 -38
- package/app/routes/admin/users/bulkUsers.action.ts +2 -9
- package/app/routes/admin/users/bulkUsers.schema.ts +1 -6
- package/app/routes/admin/users/users.route.tsx +14 -63
- package/app/routes/api/cyberduck-profile.$name.ts +6 -2
- package/app/routes/auth/callback.route.tsx +8 -33
- package/app/routes/auth/login.route.tsx +8 -11
- package/app/routes/auth/logout.route.tsx +4 -14
- package/app/routes/config.route.tsx +1 -5
- package/app/routes/connections/connection.form.tsx +5 -14
- package/app/routes/connections/connection.schema.ts +4 -16
- package/app/routes/connections/connections.loader.ts +11 -23
- package/app/routes/connections/connections.route.tsx +1 -5
- package/app/routes/connections/connections.server.ts +1 -3
- package/app/routes/connections/createConnection.action.ts +2 -8
- package/app/routes/connections/createConnection.modal.tsx +3 -13
- package/app/routes/connections/deleteConnection.action.ts +1 -4
- package/app/routes/connections/updateConnection.action.ts +2 -8
- package/app/routes/connections/updateConnection.modal.tsx +4 -14
- package/app/routes/home/home.route.tsx +8 -33
- package/app/routes/layouts/ModalOutlet.tsx +6 -18
- package/app/routes/objects/objects.loader.ts +5 -19
- package/app/routes/objects/objects.route.tsx +11 -30
- package/app/routes/presign.route.tsx +5 -18
- package/app/routes/recent.route.tsx +1 -4
- package/app/routes/search.route.tsx +4 -17
- package/app/routes.ts +1 -4
- package/app/tailwind.css +17 -12
- package/app/types/cornerstone-codecs.d.ts +25 -29
- package/app/utils/connectionsStore/selectors.ts +2 -6
- package/app/utils/connectionsStore/useConnectionsStore.ts +2 -6
- package/app/utils/db/convertCsvToParquet.ts +1 -3
- package/app/utils/db/createDatabase.ts +1 -3
- package/app/utils/db/createSingleton.ts +1 -1
- package/app/utils/db/getBlobFromObjectNode.ts +3 -9
- package/app/utils/db/getGeomQuery.ts +1 -1
- package/app/utils/db/getMarkerInfoWasm.ts +1 -3
- package/app/utils/db/getTileBoundingBox.ts +1 -4
- package/app/utils/db/sqlQueries.ts +1 -4
- package/app/utils/fileType.ts +80 -10
- package/app/utils/filterObjects.ts +2 -5
- package/app/utils/localFilesStore/useFileStore.ts +7 -7
- package/app/utils/recentlyViewed.server.ts +1 -4
- package/app/utils/resourceId.ts +3 -11
- package/app/utils/s3Provider.ts +3 -7
- package/app/utils/signedFetch.ts +4 -13
- package/bin-src/codegen.ts +4 -1
- package/package.json +5 -1
- package/prisma/seed.ts +1 -2
- package/public/favicon/site.webmanifest +1 -1
- package/server.js +1 -4
- package/server.js.map +1 -1
- package/vite-plugins/cytario-plugins.ts +2 -8
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import openjpegWasmUrl from
|
|
2
|
-
import openJpegFactory from
|
|
1
|
+
import openjpegWasmUrl from "@cornerstonejs/codec-openjpeg/decodewasm?url";
|
|
2
|
+
import openJpegFactory from "@cornerstonejs/codec-openjpeg/decodewasmjs";
|
|
3
3
|
|
|
4
4
|
const local = {
|
|
5
5
|
codec: undefined,
|
|
@@ -16,7 +16,7 @@ const local = {
|
|
|
16
16
|
*/
|
|
17
17
|
export function loadJp2KDecoder(decodeConfig) {
|
|
18
18
|
local.decodeConfig = decodeConfig;
|
|
19
|
-
|
|
19
|
+
|
|
20
20
|
// Return existing codec or loading promise
|
|
21
21
|
if (local.codec) {
|
|
22
22
|
return Promise.resolve();
|
|
@@ -28,7 +28,7 @@ export function loadJp2KDecoder(decodeConfig) {
|
|
|
28
28
|
local.isLoading = true;
|
|
29
29
|
local.loadPromise = openJpegFactory({
|
|
30
30
|
locateFile: (f) => {
|
|
31
|
-
if (typeof f ===
|
|
31
|
+
if (typeof f === "string" && f.endsWith(".wasm")) {
|
|
32
32
|
return openjpegWasmUrl;
|
|
33
33
|
}
|
|
34
34
|
return f;
|
|
@@ -57,30 +57,30 @@ export function loadJp2KDecoder(decodeConfig) {
|
|
|
57
57
|
*/
|
|
58
58
|
export async function decodeJPEG2000(compressedImageFrame) {
|
|
59
59
|
if (!compressedImageFrame || compressedImageFrame.length === 0) {
|
|
60
|
-
throw new Error(
|
|
60
|
+
throw new Error("Invalid compressed image frame: empty or null");
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
await loadJp2KDecoder();
|
|
64
|
-
|
|
64
|
+
|
|
65
65
|
if (!local.decoder) {
|
|
66
|
-
throw new Error(
|
|
66
|
+
throw new Error("JPEG2000 decoder not initialized");
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
const decoder = local.decoder;
|
|
70
|
-
|
|
70
|
+
|
|
71
71
|
// Get encoded buffer and copy data
|
|
72
72
|
const encodedBufferInWASM = decoder.getEncodedBuffer(compressedImageFrame.length);
|
|
73
73
|
encodedBufferInWASM.set(compressedImageFrame);
|
|
74
|
-
|
|
74
|
+
|
|
75
75
|
// Decode the image
|
|
76
76
|
decoder.decode();
|
|
77
|
-
|
|
77
|
+
|
|
78
78
|
// Extract decoded data
|
|
79
79
|
const frameInfo = decoder.getFrameInfo();
|
|
80
80
|
const decodedBufferInWASM = decoder.getDecodedBuffer();
|
|
81
81
|
const imageFrame = new Uint8Array(decodedBufferInWASM.length);
|
|
82
82
|
imageFrame.set(decodedBufferInWASM);
|
|
83
|
-
|
|
83
|
+
|
|
84
84
|
return {
|
|
85
85
|
info: frameInfo,
|
|
86
86
|
pixels: imageFrame,
|
|
@@ -6,71 +6,71 @@ import { decodeJPEG2000, cleanupJp2KDecoder } from "./decodeJPEG2000";
|
|
|
6
6
|
* Decoder types supported by this worker
|
|
7
7
|
*/
|
|
8
8
|
const DECODER_TYPES = {
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
LZW: "lzw-decoder",
|
|
10
|
+
JP2K: "jp2k-decoder",
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Worker message handler for image decoding tasks
|
|
15
15
|
*/
|
|
16
16
|
self.onmessage = async (event) => {
|
|
17
|
-
|
|
17
|
+
const { taskId, buffer, maxUncompressedSize, decoderId } = event.data;
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (!buffer || !decoderId) {
|
|
29
|
-
self.postMessage({
|
|
30
|
-
taskId,
|
|
31
|
-
error: 'Missing required parameters: buffer or decoderId'
|
|
32
|
-
});
|
|
33
|
-
return;
|
|
34
|
-
}
|
|
19
|
+
// Validate input
|
|
20
|
+
if (!taskId) {
|
|
21
|
+
self.postMessage({
|
|
22
|
+
taskId: "unknown",
|
|
23
|
+
error: "Missing taskId in worker message",
|
|
24
|
+
});
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
35
27
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
28
|
+
if (!buffer || !decoderId) {
|
|
29
|
+
self.postMessage({
|
|
30
|
+
taskId,
|
|
31
|
+
error: "Missing required parameters: buffer or decoderId",
|
|
32
|
+
});
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
39
35
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
throw new Error('maxUncompressedSize required for LZW decoder');
|
|
44
|
-
}
|
|
45
|
-
const decoded = await decompress(bytes, maxUncompressedSize);
|
|
46
|
-
pixels = decoded.buffer;
|
|
47
|
-
break;
|
|
48
|
-
}
|
|
36
|
+
try {
|
|
37
|
+
const bytes = new Uint8Array(buffer);
|
|
38
|
+
let pixels;
|
|
49
39
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
default:
|
|
57
|
-
throw new Error(`Unknown decoder type: ${decoderId}`);
|
|
40
|
+
switch (decoderId) {
|
|
41
|
+
case DECODER_TYPES.LZW: {
|
|
42
|
+
if (!maxUncompressedSize) {
|
|
43
|
+
throw new Error("maxUncompressedSize required for LZW decoder");
|
|
58
44
|
}
|
|
45
|
+
const decoded = await decompress(bytes, maxUncompressedSize);
|
|
46
|
+
pixels = decoded.buffer;
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
case DECODER_TYPES.JP2K: {
|
|
51
|
+
const image = await decodeJPEG2000(bytes);
|
|
52
|
+
pixels = image.pixels.buffer;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
} catch (error) {
|
|
63
|
-
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
64
|
-
self.postMessage({
|
|
65
|
-
taskId,
|
|
66
|
-
error: errorMessage
|
|
67
|
-
});
|
|
56
|
+
default:
|
|
57
|
+
throw new Error(`Unknown decoder type: ${decoderId}`);
|
|
68
58
|
}
|
|
59
|
+
|
|
60
|
+
// Transfer the ArrayBuffer to avoid copying
|
|
61
|
+
self.postMessage({ taskId, result: pixels }, [pixels]);
|
|
62
|
+
} catch (error) {
|
|
63
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
64
|
+
self.postMessage({
|
|
65
|
+
taskId,
|
|
66
|
+
error: errorMessage,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
69
|
};
|
|
70
70
|
|
|
71
71
|
/**
|
|
72
72
|
* Cleanup when worker is being terminated
|
|
73
73
|
*/
|
|
74
|
-
self.addEventListener(
|
|
75
|
-
|
|
74
|
+
self.addEventListener("close", () => {
|
|
75
|
+
cleanupJp2KDecoder();
|
|
76
76
|
});
|
|
@@ -23,117 +23,112 @@ const getWorkerPool = (): WorkerPool => {
|
|
|
23
23
|
|
|
24
24
|
// Cache for decoded blocks with LRU eviction
|
|
25
25
|
const bufferCache = new LRUCache<number, ArrayBuffer>({
|
|
26
|
-
|
|
26
|
+
max: CACHE_SIZE_LIMIT,
|
|
27
27
|
});
|
|
28
28
|
|
|
29
29
|
export interface FileDirectory {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
30
|
+
TileWidth?: number;
|
|
31
|
+
ImageWidth?: number;
|
|
32
|
+
TileLength?: number;
|
|
33
|
+
ImageLength?: number;
|
|
34
|
+
BitsPerSample: number[];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
37
|
/**
|
|
38
38
|
* Base decoder class for handling image block decoding with worker pool and caching
|
|
39
39
|
*/
|
|
40
40
|
export class GenericDecoder extends BaseDecoder {
|
|
41
|
-
|
|
41
|
+
private readonly maxUncompressedSize: number;
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
constructor(fileDirectory: FileDirectory) {
|
|
44
|
+
super();
|
|
45
45
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
46
|
+
if (!fileDirectory.BitsPerSample || fileDirectory.BitsPerSample.length === 0) {
|
|
47
|
+
throw new Error("FileDirectory must have BitsPerSample defined");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const width = fileDirectory.TileWidth || fileDirectory.ImageWidth || 0;
|
|
51
|
+
const height = fileDirectory.TileLength || fileDirectory.ImageLength || 0;
|
|
52
|
+
const nbytes = fileDirectory.BitsPerSample[0] / 8;
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
fileDirectory.TileLength || fileDirectory.ImageLength || 0;
|
|
56
|
-
const nbytes = fileDirectory.BitsPerSample[0] / 8;
|
|
54
|
+
this.maxUncompressedSize = width * height * nbytes;
|
|
55
|
+
}
|
|
57
56
|
|
|
58
|
-
|
|
57
|
+
/**
|
|
58
|
+
* Decodes a compressed image block using worker pool and caching
|
|
59
|
+
* @param inputBuffer - The compressed image block
|
|
60
|
+
* @returns Decoded image buffer
|
|
61
|
+
*/
|
|
62
|
+
async decodeBlock(inputBuffer: ArrayBuffer): Promise<ArrayBuffer> {
|
|
63
|
+
if (!inputBuffer || inputBuffer.byteLength === 0) {
|
|
64
|
+
throw new Error("Invalid input buffer: empty or null");
|
|
59
65
|
}
|
|
60
66
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
if (!inputBuffer || inputBuffer.byteLength === 0) {
|
|
68
|
-
throw new Error("Invalid input buffer: empty or null");
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const bufferHash = this.hashBuffer(inputBuffer);
|
|
72
|
-
|
|
73
|
-
// Check cache first
|
|
74
|
-
const cachedResult = bufferCache.get(bufferHash);
|
|
75
|
-
if (cachedResult) {
|
|
76
|
-
return cachedResult;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
try {
|
|
80
|
-
const outputBuffer = await getWorkerPool().runTask({
|
|
81
|
-
buffer: inputBuffer,
|
|
82
|
-
maxUncompressedSize: this.maxUncompressedSize,
|
|
83
|
-
decoderId: this.getDecoderId(),
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
// Cache the result
|
|
87
|
-
bufferCache.set(bufferHash, outputBuffer);
|
|
88
|
-
return outputBuffer;
|
|
89
|
-
} catch (error) {
|
|
90
|
-
const errorMessage =
|
|
91
|
-
error instanceof Error ? error.message : String(error);
|
|
92
|
-
throw new Error(`Failed to decode block: ${errorMessage}`);
|
|
93
|
-
}
|
|
67
|
+
const bufferHash = this.hashBuffer(inputBuffer);
|
|
68
|
+
|
|
69
|
+
// Check cache first
|
|
70
|
+
const cachedResult = bufferCache.get(bufferHash);
|
|
71
|
+
if (cachedResult) {
|
|
72
|
+
return cachedResult;
|
|
94
73
|
}
|
|
95
74
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
75
|
+
try {
|
|
76
|
+
const outputBuffer = await getWorkerPool().runTask({
|
|
77
|
+
buffer: inputBuffer,
|
|
78
|
+
maxUncompressedSize: this.maxUncompressedSize,
|
|
79
|
+
decoderId: this.getDecoderId(),
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// Cache the result
|
|
83
|
+
bufferCache.set(bufferHash, outputBuffer);
|
|
84
|
+
return outputBuffer;
|
|
85
|
+
} catch (error) {
|
|
86
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
87
|
+
throw new Error(`Failed to decode block: ${errorMessage}`);
|
|
101
88
|
}
|
|
89
|
+
}
|
|
102
90
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Returns the decoder identifier (must be overridden by subclasses)
|
|
93
|
+
*/
|
|
94
|
+
public getDecoderId(): string {
|
|
95
|
+
return "uninitialized-decoder";
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* FNV-1a hash function for ArrayBuffer - faster and better distribution than simple hash
|
|
100
|
+
* @param buffer - The buffer to hash
|
|
101
|
+
* @returns Hash value
|
|
102
|
+
*/
|
|
103
|
+
private hashBuffer(buffer: ArrayBuffer): number {
|
|
104
|
+
const FNV_OFFSET_BASIS = 2166136261;
|
|
105
|
+
const FNV_PRIME = 16777619;
|
|
106
|
+
|
|
107
|
+
let hash = FNV_OFFSET_BASIS;
|
|
108
|
+
const view = new Uint8Array(buffer);
|
|
109
|
+
const len = view.length;
|
|
110
|
+
|
|
111
|
+
for (let i = 0; i < len; i++) {
|
|
112
|
+
hash ^= view[i];
|
|
113
|
+
hash = Math.imul(hash, FNV_PRIME);
|
|
122
114
|
}
|
|
115
|
+
|
|
116
|
+
return hash >>> 0; // Convert to unsigned 32-bit integer
|
|
117
|
+
}
|
|
123
118
|
}
|
|
124
119
|
|
|
125
120
|
/**
|
|
126
121
|
* Clears the decoder cache
|
|
127
122
|
*/
|
|
128
123
|
export function clearDecoderCache(): void {
|
|
129
|
-
|
|
124
|
+
bufferCache.clear();
|
|
130
125
|
}
|
|
131
126
|
|
|
132
127
|
/**
|
|
133
128
|
* Terminates the worker pool and releases resources
|
|
134
129
|
*/
|
|
135
130
|
export function shutdownDecoderPool(): void {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
131
|
+
workerPool?.terminate();
|
|
132
|
+
workerPool = null;
|
|
133
|
+
bufferCache.clear();
|
|
139
134
|
}
|
|
@@ -5,14 +5,14 @@ import { GenericDecoder, FileDirectory } from "./genericDecoder";
|
|
|
5
5
|
* Used for decoding JPEG2000-compressed image data
|
|
6
6
|
*/
|
|
7
7
|
export class JP2KDecoder extends GenericDecoder {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
constructor(fileDirectory: FileDirectory) {
|
|
9
|
+
super(fileDirectory);
|
|
10
|
+
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Returns the decoder identifier for the worker pool
|
|
14
|
+
*/
|
|
15
|
+
public getDecoderId(): string {
|
|
16
|
+
return "jp2k-decoder";
|
|
17
|
+
}
|
|
18
18
|
}
|
|
@@ -5,14 +5,14 @@ import { GenericDecoder, FileDirectory } from "./genericDecoder";
|
|
|
5
5
|
* Used for decoding LZW-compressed TIFF image data
|
|
6
6
|
*/
|
|
7
7
|
export class LZWDecoder extends GenericDecoder {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
constructor(fileDirectory: FileDirectory) {
|
|
9
|
+
super(fileDirectory);
|
|
10
|
+
}
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Returns the decoder identifier for the worker pool
|
|
14
|
+
*/
|
|
15
|
+
public getDecoderId(): string {
|
|
16
|
+
return "lzw-decoder";
|
|
17
|
+
}
|
|
18
18
|
}
|
package/app/components/.client/ImageViewer/state/loaders/loadBioformatsZarrWithCredentials.ts
CHANGED
|
@@ -59,15 +59,11 @@ export function rootAttrsToImage(rootAttrs: RootAttrs, loader: Loader): Image {
|
|
|
59
59
|
const SizeT = shape[dimIndex("t")] ?? 1;
|
|
60
60
|
const SizeC = shape[dimIndex("c")] ?? (channels.length || 1);
|
|
61
61
|
|
|
62
|
-
const { PhysicalSizeX, PhysicalSizeY, PhysicalSizeZ } = extractPhysicalSizes(
|
|
63
|
-
multiscale,
|
|
64
|
-
axes,
|
|
65
|
-
);
|
|
62
|
+
const { PhysicalSizeX, PhysicalSizeY, PhysicalSizeZ } = extractPhysicalSizes(multiscale, axes);
|
|
66
63
|
|
|
67
64
|
const axisUnit = (name: string) => {
|
|
68
65
|
const axis = axes.find(
|
|
69
|
-
(a): a is { name: string; unit?: string } =>
|
|
70
|
-
typeof a !== "string" && a.name === name,
|
|
66
|
+
(a): a is { name: string; unit?: string } => typeof a !== "string" && a.name === name,
|
|
71
67
|
);
|
|
72
68
|
return axis?.unit ?? "µm";
|
|
73
69
|
};
|
|
@@ -94,13 +90,11 @@ export function rootAttrsToImage(rootAttrs: RootAttrs, loader: Loader): Image {
|
|
|
94
90
|
PhysicalSizeXUnit: axisUnit("x"),
|
|
95
91
|
PhysicalSizeYUnit: axisUnit("y"),
|
|
96
92
|
PhysicalSizeZUnit: axisUnit("z"),
|
|
97
|
-
Channels: channels.map(
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}),
|
|
103
|
-
),
|
|
93
|
+
Channels: channels.map((ch: { label: string; color: string }, i: number) => ({
|
|
94
|
+
ID: `Channel:0:${i}`,
|
|
95
|
+
Name: ch.label,
|
|
96
|
+
Color: parseOmeroColor(ch.color),
|
|
97
|
+
})),
|
|
104
98
|
},
|
|
105
99
|
} as Image;
|
|
106
100
|
}
|
|
@@ -112,18 +106,14 @@ export function extractPhysicalSizes(
|
|
|
112
106
|
) {
|
|
113
107
|
const datasets = (multiscale?.datasets ?? []) as DatasetWithTransforms[];
|
|
114
108
|
const firstDataset = datasets[0];
|
|
115
|
-
const scaleTransform = firstDataset?.coordinateTransformations?.find(
|
|
116
|
-
(t) => t.type === "scale",
|
|
117
|
-
);
|
|
109
|
+
const scaleTransform = firstDataset?.coordinateTransformations?.find((t) => t.type === "scale");
|
|
118
110
|
|
|
119
111
|
if (!scaleTransform?.scale || !axes) {
|
|
120
112
|
return {};
|
|
121
113
|
}
|
|
122
114
|
|
|
123
115
|
const resolvedAxes =
|
|
124
|
-
axes.map((a: string | { name: string }) =>
|
|
125
|
-
typeof a === "string" ? a : a.name,
|
|
126
|
-
) ?? [];
|
|
116
|
+
axes.map((a: string | { name: string }) => (typeof a === "string" ? a : a.name)) ?? [];
|
|
127
117
|
|
|
128
118
|
const scaleForAxis = (name: string): number | undefined => {
|
|
129
119
|
const idx = resolvedAxes.indexOf(name);
|
|
@@ -141,9 +131,7 @@ export function extractPhysicalSizes(
|
|
|
141
131
|
* Omero colors are hex strings — "FF0000" (RGB) or "FF0000FF" (RGBA).
|
|
142
132
|
* Exported for testing.
|
|
143
133
|
*/
|
|
144
|
-
export function parseOmeroColor(
|
|
145
|
-
color: string,
|
|
146
|
-
): [number, number, number, number] | undefined {
|
|
134
|
+
export function parseOmeroColor(color: string): [number, number, number, number] | undefined {
|
|
147
135
|
if (!color) return undefined;
|
|
148
136
|
|
|
149
137
|
const hex = color.replace(/^#/, "");
|
|
@@ -82,32 +82,18 @@ interface ViewerStoreProviderProps {
|
|
|
82
82
|
}
|
|
83
83
|
|
|
84
84
|
// Viewer is auth-agnostic — caller owns URL construction and signing.
|
|
85
|
-
export const ViewerStoreProvider = ({
|
|
86
|
-
url,
|
|
87
|
-
signedFetch,
|
|
88
|
-
children,
|
|
89
|
-
}: ViewerStoreProviderProps) => {
|
|
85
|
+
export const ViewerStoreProvider = ({ url, signedFetch, children }: ViewerStoreProviderProps) => {
|
|
90
86
|
const registerViewer = useViewerRegistryStore((s) => s.registerViewer);
|
|
91
87
|
|
|
92
|
-
const store = useMemo(
|
|
93
|
-
() => registerViewer(url, signedFetch),
|
|
94
|
-
[url, signedFetch, registerViewer],
|
|
95
|
-
);
|
|
88
|
+
const store = useMemo(() => registerViewer(url, signedFetch), [url, signedFetch, registerViewer]);
|
|
96
89
|
|
|
97
|
-
return
|
|
98
|
-
<ViewerStoreContext.Provider value={store}>
|
|
99
|
-
{children}
|
|
100
|
-
</ViewerStoreContext.Provider>
|
|
101
|
-
);
|
|
90
|
+
return <ViewerStoreContext.Provider value={store}>{children}</ViewerStoreContext.Provider>;
|
|
102
91
|
};
|
|
103
92
|
|
|
104
93
|
/** Access the viewer store from within a ViewerStoreProvider. */
|
|
105
94
|
export const useViewerStore = <T,>(selector: (state: ViewerStore) => T): T => {
|
|
106
95
|
const store = useContext(ViewerStoreContext);
|
|
107
96
|
|
|
108
|
-
if (!store)
|
|
109
|
-
throw new Error(
|
|
110
|
-
"useViewerStoreContext must be used within ViewerStoreProvider",
|
|
111
|
-
);
|
|
97
|
+
if (!store) throw new Error("useViewerStoreContext must be used within ViewerStoreProvider");
|
|
112
98
|
return useStore(store, selector);
|
|
113
99
|
};
|