88 Target ,
99 Upload ,
1010 AlertCircle ,
11- RefreshCw
11+ RefreshCw ,
12+ Info
1213} from 'lucide-react' ;
1314
1415interface Annotation {
@@ -56,7 +57,7 @@ export const ImageWorkbenchNode: React.FC<NodeViewProps> = ({ node, updateAttrib
5657 return utifLoadPromise ;
5758 } ;
5859
59- // Convert TIFF ArrayBuffer to browser-readable PNG Data URL
60+ // Convert TIFF ArrayBuffer to browser-readable PNG/JPEG Data URL
6061 const processTiff = async ( buffer : ArrayBuffer ) => {
6162 console . log ( "[TIFF] Starting decoding, input size:" , ( buffer . byteLength / 1024 ) . toFixed ( 1 ) , "KB" ) ;
6263 await loadUTIF ( ) ;
@@ -67,44 +68,78 @@ export const ImageWorkbenchNode: React.FC<NodeViewProps> = ({ node, updateAttrib
6768 console . log ( "[TIFF] IFDs found:" , ifds . length ) ;
6869
6970 if ( ! ifds || ifds . length === 0 ) {
70- throw new Error ( "The TIFF file appears to be empty or corrupted." ) ;
71+ throw new Error ( "The TIFF file appears to be empty or corrupted (no IFDs found) ." ) ;
7172 }
7273
73- // Find the first IFD with image dimensions (skip thumbnails/metadata if possible)
74- let targetIFD = ifds . find ( ( i : any ) => i . width > 200 && i . height > 200 ) ;
75- if ( ! targetIFD ) targetIFD = ifds . find ( ( i : any ) => i . width && i . height ) ;
76- if ( ! targetIFD ) targetIFD = ifds [ 0 ] ;
74+ // Find the "best" IFD. For scientific images, this is usually the one with the largest dimensions.
75+ // We also look for those with standard pixel types (tPixel).
76+ const sortedIFDs = [ ... ifds ] . sort ( ( a : any , b : any ) => ( b . width * b . height ) - ( a . width * a . height ) ) ;
77+ const targetIFD = sortedIFDs [ 0 ] ;
7778
7879 if ( ! targetIFD . width || ! targetIFD . height ) {
79- console . error ( "[TIFF] Invalid target IFD:" , targetIFD ) ;
80- throw new Error ( "Could not detect image dimensions in the TIFF structure." ) ;
80+ console . error ( "[TIFF] Invalid IFD data :" , targetIFD ) ;
81+ throw new Error ( `Invalid TIFF structure: Width= ${ targetIFD . width } , Height= ${ targetIFD . height } ` ) ;
8182 }
8283
83- console . log ( "[TIFF] Decoding IFD :" , { width : targetIFD . width , height : targetIFD . height , tPixel : targetIFD . tPixel } ) ;
84+ console . log ( "[TIFF] Target Image Dimensions :" , targetIFD . width , "x" , targetIFD . height ) ;
8485 UTIF . decodeImage ( buffer , targetIFD ) ;
8586
86- const rgba = UTIF . toRGBA8 ( targetIFD ) ;
87+ let rgba = UTIF . toRGBA8 ( targetIFD ) ;
8788 console . log ( "[TIFF] RGBA conversion complete, buffer length:" , rgba . length ) ;
88-
89+
90+ // --- ADAPTIVE DOWNSCALING ---
91+ // Browsers have canvas limits (often 4096px or 8192px).
92+ // Large microscopy images can hit these or cause memory overflows.
93+ const MAX_DIMENSION = 2048 ; // Safe threshold for notebook performance
94+ let scale = 1 ;
95+ if ( targetIFD . width > MAX_DIMENSION || targetIFD . height > MAX_DIMENSION ) {
96+ scale = Math . min ( MAX_DIMENSION / targetIFD . width , MAX_DIMENSION / targetIFD . height ) ;
97+ }
98+
8999 const canvas = document . createElement ( 'canvas' ) ;
90- canvas . width = targetIFD . width ;
91- canvas . height = targetIFD . height ;
100+ const finalWidth = Math . floor ( targetIFD . width * scale ) ;
101+ const finalHeight = Math . floor ( targetIFD . height * scale ) ;
102+
103+ canvas . width = finalWidth ;
104+ canvas . height = finalHeight ;
92105 const ctx = canvas . getContext ( '2d' ) ;
93- if ( ! ctx ) throw new Error ( "Could not initialize conversion canvas." ) ;
106+ if ( ! ctx ) throw new Error ( "Could not initialize conversion canvas (likely browser memory limit)." ) ;
107+
108+ if ( scale < 1 ) {
109+ console . log ( "[TIFF] Scaling down image:" , scale . toFixed ( 2 ) , "x ->" , finalWidth , "x" , finalHeight ) ;
110+ // Temporarily put full image on a hidden canvas to use drawImage for high-quality scaling
111+ const tempCanvas = document . createElement ( 'canvas' ) ;
112+ tempCanvas . width = targetIFD . width ;
113+ tempCanvas . height = targetIFD . height ;
114+ const tempCtx = tempCanvas . getContext ( '2d' ) ;
115+ if ( ! tempCtx ) throw new Error ( "Could not initialize temporary scaling canvas." ) ;
116+
117+ const imgData = tempCtx . createImageData ( targetIFD . width , targetIFD . height ) ;
118+ imgData . data . set ( new Uint8ClampedArray ( rgba ) ) ;
119+ tempCtx . putImageData ( imgData , 0 , 0 ) ;
120+
121+ // Final draw with scaling
122+ ctx . imageSmoothingEnabled = true ;
123+ ctx . imageSmoothingQuality = 'high' ;
124+ ctx . drawImage ( tempCanvas , 0 , 0 , finalWidth , finalHeight ) ;
125+ } else {
126+ const imgData = ctx . createImageData ( canvas . width , canvas . height ) ;
127+ imgData . data . set ( new Uint8ClampedArray ( rgba ) ) ;
128+ ctx . putImageData ( imgData , 0 , 0 ) ;
129+ }
94130
95- const imgData = ctx . createImageData ( canvas . width , canvas . height ) ;
96- imgData . data . set ( new Uint8ClampedArray ( rgba ) ) ;
97- ctx . putImageData ( imgData , 0 , 0 ) ;
131+ // Cleanup large buffers immediately
132+ rgba = null ;
98133
99- // Use image/jpeg for large images to save space/bandwidth if over 4MB
100- const type = ( canvas . width * canvas . height > 2000000 ) ? 'image/jpeg' : 'image/png' ;
101- const quality = type === 'image/jpeg' ? 0.92 : undefined ;
134+ // Use image/jpeg for large images to save space (bloating data attributes breaks Tiptap/Supabase)
135+ const type = ( finalWidth * finalHeight > 1000000 ) ? 'image/jpeg' : 'image/png' ;
136+ const quality = type === 'image/jpeg' ? 0.85 : undefined ;
102137
103138 const dataUrl = canvas . toDataURL ( type , quality ) ;
104- console . log ( "[TIFF] Data URL generated successfully, type:" , type , "length:" , ( dataUrl . length / 1024 ) . toFixed ( 1 ) , "KB" ) ;
139+ console . log ( "[TIFF] Success, data URL type:" , type , "length:" , ( dataUrl . length / 1024 ) . toFixed ( 1 ) , "KB" ) ;
105140 return dataUrl ;
106141 } catch ( error : unknown ) {
107- console . error ( "[TIFF] Error during processing :" , error ) ;
142+ console . error ( "[TIFF] Critical Processing Error :" , error ) ;
108143 throw error ;
109144 }
110145 } ;
@@ -121,15 +156,16 @@ export const ImageWorkbenchNode: React.FC<NodeViewProps> = ({ node, updateAttrib
121156
122157 if ( isTiff ) {
123158 setIsDecoding ( true ) ;
124- console . log ( "[TIFF] Uploading scientific file :" , fileName ) ;
159+ console . log ( "[TIFF] Upload Request :" , fileName , `( ${ ( file . size / 1024 / 1024 ) . toFixed ( 2 ) } MB)` ) ;
125160 try {
126161 const buffer = await file . arrayBuffer ( ) ;
127162 const dataUrl = await processTiff ( buffer ) ;
163+ console . log ( "[TIFF] Rendering to workbench..." ) ;
128164 updateAttributes ( { src : dataUrl } ) ;
129165 } catch ( err : unknown ) {
130- const msg = err instanceof Error ? err . message : 'Unknown error' ;
131- console . error ( "[TIFF] Error processing upload:" , msg ) ;
132- setImageLoadError ( `TIFF Processing Error : ${ msg } ` ) ;
166+ const msg = err instanceof Error ? err . message : 'Unknown decoding error' ;
167+ console . error ( "[TIFF] Failed to process upload:" , msg ) ;
168+ setImageLoadError ( `TIFF Processing Failed : ${ msg } . Try a different file or export as PNG/JPG. ` ) ;
133169 } finally {
134170 setIsDecoding ( false ) ;
135171 }
@@ -195,6 +231,7 @@ export const ImageWorkbenchNode: React.FC<NodeViewProps> = ({ node, updateAttrib
195231 const calculateDistance = useCallback ( ( points : { x : number , y : number } [ ] ) => {
196232 const p1 = points [ 0 ] ;
197233 const p2 = points [ 1 ] ;
234+ if ( ! p1 || ! p2 ) return '0.0 px' ;
198235 const dx = p1 . x - p2 . x ;
199236 const dy = p1 . y - p2 . y ;
200237 const distPx = Math . sqrt ( dx * dx + dy * dy ) ;
@@ -223,11 +260,15 @@ export const ImageWorkbenchNode: React.FC<NodeViewProps> = ({ node, updateAttrib
223260 if ( ! isDrawing ) return ;
224261 setIsDrawing ( false ) ;
225262
263+ if ( currentPoints . length < 2 ) {
264+ setCurrentPoints ( [ ] ) ;
265+ return ;
266+ }
267+
226268 if ( activeTool === 'calibrate' ) {
227269 const umStr = window . prompt ( 'Enter real-world length (µm) for this line:' , '100' ) ;
228270 const um = parseFloat ( umStr || '0' ) ;
229271 if ( ! isNaN ( um ) && um > 0 ) {
230- if ( currentPoints . length < 2 ) return ;
231272 const dx = currentPoints [ 0 ] . x - currentPoints [ 1 ] . x ;
232273 const dy = currentPoints [ 0 ] . y - currentPoints [ 1 ] . y ;
233274 const pxDist = Math . sqrt ( dx * dx + dy * dy ) ;
@@ -236,7 +277,6 @@ export const ImageWorkbenchNode: React.FC<NodeViewProps> = ({ node, updateAttrib
236277 } ) ;
237278 }
238279 } else if ( activeTool === 'measure' || activeTool === 'roi' ) {
239- if ( currentPoints . length < 2 ) return ;
240280 const newAnnotation : Annotation = {
241281 id : Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) ,
242282 type : activeTool ,
@@ -368,54 +408,78 @@ export const ImageWorkbenchNode: React.FC<NodeViewProps> = ({ node, updateAttrib
368408 src = { src }
369409 alt = "Scientific Sample"
370410 className = "max-w-full h-auto select-none pointer-events-none"
371- onError = { ( ) => setImageLoadError ( "Failed to render decoded image. The file might be too large or uses an unsupported compression ." ) }
411+ onError = { ( ) => setImageLoadError ( "Browser failed to render the decoded binary. This can happen with massive files or corrupt streams ." ) }
372412 />
373413 { imageLoadError && (
374- < div className = "absolute inset-0 flex flex-col items-center justify-center bg-black/70 backdrop-blur-sm z-20 p-8 text-center" >
375- < div className = "p-3 bg-red-500/20 text-red-500 rounded-full mb-4 " >
376- < AlertCircle className = "w-8 h-8 " />
414+ < div className = "absolute inset-0 flex flex-col items-center justify-center bg-black/80 backdrop-blur-md z-30 p-10 text-center" >
415+ < div className = "p-4 bg-red-500/20 text-red-500 rounded-2xl mb-6 shadow-glow-red " >
416+ < AlertCircle className = "w-10 h-10 " />
377417 </ div >
378- < h3 className = "text-white font-bold mb-2 text-sm uppercase tracking-wider" > Image Analysis Error</ h3 >
379- < p className = "text-neutral-400 text-[10px] max-w-xs" > { imageLoadError } </ p >
380- < button
418+ < h3 className = "text-white font-bold mb-3 text-lg tracking-tight" > TIFF Rendering Error</ h3 >
419+ < p className = "text-neutral-400 text-xs max-w-sm leading-relaxed mb-8" > { imageLoadError } </ p >
420+
421+ < div className = "flex flex-col gap-3 w-full max-w-xs" >
422+ < button
381423 onClick = { ( ) => fileInputRef . current ?. click ( ) }
382- className = "mt-6 px-4 py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-xl text-xs font-bold transition-all shadow-lg flex items-center gap-2"
424+ className = "px-6 py-3 bg-blue-600 hover:bg-blue-500 text-white rounded-xl text-sm font-bold transition-all shadow-lg flex items-center justify-center gap-2"
425+ >
426+ < Upload className = "w-4 h-4" />
427+ Try Different File
428+ </ button >
429+ < button
430+ onClick = { ( ) => window . location . reload ( ) }
431+ className = "px-6 py-2 bg-neutral-800 hover:bg-neutral-700 text-neutral-300 rounded-xl text-xs transition-all flex items-center justify-center gap-2"
383432 >
384433 < RefreshCw className = "w-3 h-3" />
385- Try Different Image
434+ Reload App
386435 </ button >
436+ </ div >
437+
438+ < div className = "mt-8 pt-8 border-t border-white/5 flex items-center gap-2 text-neutral-500" >
439+ < Info className = "w-3 h-3" />
440+ < span className = "text-[10px] italic" > Check the browser console for exact error details</ span >
441+ </ div >
387442 </ div >
388443 ) }
389444 < canvas
390445 ref = { canvasRef }
391446 className = "absolute inset-0 pointer-events-none"
392447 />
393448 { isDecoding && (
394- < div className = "absolute inset-0 flex flex-col items-center justify-center bg-black/80 backdrop-blur-md z-40" >
395- < div className = "w-12 h-12 border-4 border-blue-500 border-t-transparent rounded-full animate-spin mb-4" />
396- < p className = "text-white text-xs font-bold uppercase tracking-widest animate-pulse" > Decoding TIFF Layers...</ p >
397- < p className = "text-neutral-500 text-[9px] mt-2 italic text-center px-6" > Parsing scientific bit-depth for web visualization. This may take a moment for large microscopy files.</ p >
449+ < div className = "absolute inset-0 flex flex-col items-center justify-center bg-black/85 backdrop-blur-xl z-40" >
450+ < div className = "relative w-16 h-16 mb-6" >
451+ < div className = "absolute inset-0 border-4 border-blue-500/20 rounded-full" />
452+ < div className = "absolute inset-0 border-4 border-blue-500 border-t-transparent rounded-full animate-spin" />
453+ </ div >
454+ < p className = "text-white text-xs font-bold uppercase tracking-[0.2em] animate-pulse" > Decoding Scientific Data</ p >
455+ < div className = "flex flex-col items-center mt-3 text-neutral-500 text-[10px] space-y-1" >
456+ < span > Converting high bit-depth TIFF to visual preview</ span >
457+ < span > Applying adaptive scaling for workbench stability</ span >
458+ </ div >
398459 </ div >
399460 ) }
400461 </ >
401462 ) : (
402463 < div
403- className = "flex flex-col items-center gap-4 text-neutral-500 hover:text-blue-400 transition-colors cursor-pointer"
464+ className = "flex flex-col items-center gap-4 text-neutral-500 hover:text-blue-400 transition-all cursor-pointer p-12 text-center "
404465 onClick = { ( ) => fileInputRef . current ?. click ( ) }
405466 >
406- < div className = "w-20 h-20 rounded-full bg-neutral-800 flex items-center justify-center border-2 border-dashed border-neutral-700 group-hover:border-blue-500/50 shadow-inner group-hover:scale-110 transition-transform duration-300" >
407- < Upload className = "w-10 h-10" />
467+ < div className = "w-24 h-24 rounded-3xl bg-neutral-800 flex items-center justify-center border-2 border-dashed border-neutral-700 group-hover:border-blue-500/50 shadow-inner group-hover:scale-105 transition-all duration-300 relative overflow-hidden" >
468+ < div className = "absolute inset-0 bg-blue-500/5 opacity-0 group-hover:opacity-100 transition-opacity" />
469+ < Upload className = "w-10 h-10 group-hover:translate-y--2 transition-transform" />
408470 </ div >
409- < div className = "text-center" >
410- < p className = "text-sm font-bold text-neutral-300" > Drop Scientific Image</ p >
411- < p className = "text-[10px] text-neutral-500 mt-1 uppercase tracking-tighter" > Supports PNG, JPG, TIF, TIFF</ p >
471+ < div >
472+ < p className = "text-lg font-bold text-neutral-200" > Scientific Image Workbench</ p >
473+ < p className = "text-xs text-neutral-500 mt-2 max-w-xs leading-relaxed" >
474+ Drag and drop your samples here. Supports raw < span className = "text-blue-400/80 font-mono" > .tif</ span > , < span className = "text-blue-400/80 font-mono" > .tiff</ span > , PNG, and JPEG.
475+ </ p >
412476 </ div >
413477 </ div >
414478 ) }
415479
416480 { /* TOOLBAR */ }
417481 { src && ! isDecoding && (
418- < div className = "absolute top-4 left-1/2 -translate-x-1/2 flex items-center gap-1 p-1.5 bg-black/60 backdrop-blur-md border border-white/10 rounded-2xl opacity-0 group-hover:opacity-100 transition-all duration-300 transform group-hover:translate-y-0 translate-y-2 shadow-2xl z-30" >
482+ < div className = "absolute top-4 left-1/2 -translate-x-1/2 flex items-center gap-1 p-1.5 bg-black/80 backdrop-blur-lg border border-white/10 rounded-2xl opacity-0 group-hover:opacity-100 transition-all duration-300 transform group-hover:translate-y-0 translate-y-2 shadow-2xl z-30" >
419483 < ToolButton
420484 active = { activeTool === 'select' }
421485 onClick = { ( ) => setActiveTool ( 'select' ) }
@@ -460,19 +524,22 @@ export const ImageWorkbenchNode: React.FC<NodeViewProps> = ({ node, updateAttrib
460524 { /* STATUS BAR */ }
461525 { src && ! imageLoadError && ! isDecoding && (
462526 < div className = "absolute bottom-4 left-4 right-4 flex items-center justify-between pointer-events-none opacity-0 group-hover:opacity-100 transition-opacity" >
463- < div className = "flex items-center gap-3 px-3 py-1.5 bg-black/60 backdrop-blur-md border border-white/10 rounded-full text-[10px] font-mono text-neutral-400 pointer-events-auto" >
464- < div className = "flex items-center gap-1.5" >
465- < div className = { `w-1.5 h-1.5 rounded-full ${ calibration . ratio > 0 ? 'bg-emerald-500' : 'bg-amber-500' } ` } />
466- < span > { calibration . ratio > 0 ? `Calibrated: 1px = ${ calibration . ratio . toFixed ( 2 ) } µm` : 'Uncalibrated' } </ span >
527+ < div className = "flex items-center gap-3 px-4 py-2 bg-black/80 backdrop-blur-lg border border-white/10 rounded-full text-[10px] font-mono text-neutral-300 pointer-events-auto shadow-lg" >
528+ < div className = "flex items-center gap-2" >
529+ < div className = { `w-2 h-2 rounded-full ${ calibration . ratio > 0 ? 'bg-emerald-500 shadow-glow-emerald' : 'bg-amber-500 shadow-glow-amber' } ` } />
530+ < span className = "tracking-tight uppercase" > { calibration . ratio > 0 ? `Scale: 1px = ${ calibration . ratio . toFixed ( 2 ) } µm` : 'Uncalibrated' } </ span >
531+ </ div >
532+ < div className = "w-px h-4 bg-white/15" />
533+ < div className = "flex items-center gap-1.5 opacity-80" >
534+ < Target className = "w-3 h-3 text-blue-400" />
535+ < span > { annotations . length } Annotations</ span >
467536 </ div >
468- < div className = "w-px h-3 bg-white/10" />
469- < span > { annotations . length } Annotations</ span >
470537 </ div >
471538
472539 { calibration . ratio > 0 && (
473- < div className = "flex items-center gap-2 px-3 py-1 bg-white/5 backdrop-blur-sm rounded-lg border border-white/10" >
474- < div className = "w-10 h-px bg-white" />
475- < span className = "text-[10px] text-white font-mono" > { calibration . um } µm</ span >
540+ < div className = "flex items-center gap-3 px-4 py-2 bg-white/5 backdrop-blur-md rounded-2xl border border-white/10 shadow-lg " >
541+ < div className = "w-8 h-[2px] bg-white/80 rounded-full " />
542+ < span className = "text-[10px] text-white font-mono font-bold tracking-widest " > { calibration . um } µm</ span >
476543 </ div >
477544 ) }
478545 </ div >
@@ -493,9 +560,9 @@ const ToolButton: React.FC<{ active: boolean, onClick: () => void, icon: React.R
493560 < button
494561 onClick = { ( e ) => { e . preventDefault ( ) ; e . stopPropagation ( ) ; onClick ( ) ; } }
495562 title = { label }
496- className = { `p-2 rounded-xl flex items-center justify-center transition-all ${
563+ className = { `p-2.5 rounded-xl flex items-center justify-center transition-all duration-200 ${
497564 active
498- ? 'bg-blue-500/20 text-blue-400 shadow-[0_0_15px_rgba (59,130,246,0.3)]'
565+ ? 'bg-blue-500/20 text-blue-400 shadow-[0_0_20px_rgba (59,130,246,0.3)] ring-1 ring-blue-500/40 '
499566 : 'text-neutral-400 hover:bg-white/10 hover:text-white'
500567 } `}
501568 >
0 commit comments