11use std:: sync:: Arc ;
22
3+ use arrow:: array:: AsArray ;
34use arrow:: compute:: concat_batches;
5+ use arrow:: datatypes:: DataType ;
46use arrow:: record_batch:: RecordBatch ;
7+ use arrow_cast:: base64:: { BASE64_STANDARD , Engine } ;
58use arrow_cast:: display:: array_value_to_string;
69use datafusion:: physical_plan:: { ExecutionPlan , SendableRecordBatchStream } ;
710use dioxus:: prelude:: * ;
811use futures:: StreamExt ;
12+ use mimetype_detector:: detect;
913
1014use crate :: components:: ui:: Panel ;
1115use crate :: utils:: { export_to_csv_inner, export_to_parquet_inner, format_arrow_type} ;
@@ -61,6 +65,9 @@ pub fn QueryResultView(
6165 let record_batches = use_signal ( Vec :: < RecordBatch > :: new) ;
6266 let remaining_stream = use_signal ( || None :: < SendableRecordBatchStream > ) ;
6367
68+ let mut decode_images = use_signal ( || false ) ;
69+ let mut expanded_image_url = use_signal ( || None :: < Arc < str > > ) ;
70+
6471 if !initialized ( ) {
6572 initialized. set ( true ) ;
6673 let query = query. clone ( ) ;
@@ -204,6 +211,12 @@ pub fn QueryResultView(
204211 onclick: move |_| on_hide. call( id) ,
205212 "Hide"
206213 }
214+ button {
215+ class: if decode_images( ) { "btn btn-xs btn-primary" } else { "btn btn-xs btn-ghost" } ,
216+ title: "Decode bytes as images" ,
217+ onclick: move |_| decode_images. set( !decode_images( ) ) ,
218+ "Decode bytes as images"
219+ }
207220 }
208221 }
209222 }
@@ -221,6 +234,18 @@ pub fn QueryResultView(
221234 div { class: "mb-4" , { physical_plan_view( plan) } }
222235 }
223236
237+ if let Some ( url) = expanded_image_url( ) {
238+ div {
239+ class: "modal modal-open" ,
240+ onclick: move |_| expanded_image_url. set( None ) ,
241+ div {
242+ class: "modal-box w-fit max-w-[80vw] max-h-[80vh] overflow-auto" ,
243+ onclick: move |ev| ev. stop_propagation( ) ,
244+ img { src: "{url}" }
245+ }
246+ }
247+ }
248+
224249 if batches. is_empty( ) {
225250 div { class: "text-xs text-base-content opacity-75" ,
226251 "Query executed successfully, no rows returned."
@@ -235,6 +260,7 @@ pub fn QueryResultView(
235260 let schema = merged_record_batch. schema( ) ;
236261 let total_rows = merged_record_batch. num_rows( ) ;
237262 let show_rows = visible_rows( ) . min( total_rows) ;
263+ let decode_images = decode_images( ) ;
238264 rsx! {
239265 div { class: "max-h-[32rem] overflow-auto overflow-x-auto relative" ,
240266 table { class: "table table-zebra table-pin-rows table-xs" ,
@@ -261,9 +287,43 @@ pub fn QueryResultView(
261287 let cell_value = array_value_to_string( column. as_ref( ) , row_idx)
262288 . unwrap_or_else( |_| "NULL" . to_string( ) ) ;
263289 let preview = cell_value. chars( ) . take( 200 ) . collect:: <String >( ) ;
290+
291+ let image_data_url: Option <String > = if decode_images {
292+ let column_value: Option <& [ u8 ] > = if column. is_null( row_idx) {
293+ None
294+ } else {
295+ match column. data_type( ) {
296+ DataType :: BinaryView => Some ( column. as_binary_view( ) . value( row_idx) ) ,
297+ DataType :: Binary => Some ( column. as_binary:: <i32 >( ) . value( row_idx) ) ,
298+ DataType :: LargeBinary => Some ( column. as_binary:: <i64 >( ) . value( row_idx) ) ,
299+ _ => None ,
300+ }
301+ } ;
302+
303+ column_value. and_then( |bytes| {
304+ let mime = detect( bytes) ;
305+ if !mime. kind( ) . is_image( ) {
306+ return None ;
307+ }
308+
309+ let b64_string = BASE64_STANDARD . encode( bytes) ;
310+ Some ( format!( "data:{};base64,{}" , mime. mime( ) , b64_string) )
311+ } )
312+ } else {
313+ None
314+ } ;
264315 rsx! {
265316 td { class: "px-1 py-1 leading-tight break-words" ,
266- if cell_value. len( ) > 200 {
317+ if let Some ( url) = & image_data_url {
318+ img {
319+ class: "max-h-24 max-w-xs object-contain cursor-pointer hover:opacity-80 transition-opacity" ,
320+ src: "{url}" ,
321+ onclick: {
322+ let url = Arc :: from( url. as_str( ) ) ;
323+ move |_| expanded_image_url. set( Some ( Arc :: clone( & url) ) )
324+ } ,
325+ }
326+ } else if cell_value. len( ) > 200 {
267327 details {
268328 summary { class: "cursor-pointer select-none" , "{preview}..." }
269329 pre { class: "whitespace-pre-wrap" , "{cell_value}" }
0 commit comments