@@ -789,6 +789,106 @@ static void _update_preview(_object_data_t *d)
789789 }
790790}
791791
792+ // save the raster mask as an RGB PNG to the raster mask root folder
793+ // (compatible with the external raster masks module)
794+ static void _save_raster_mask (const float * mask ,
795+ const int w ,
796+ const int h ,
797+ const float threshold )
798+ {
799+ if (!mask || w <= 0 || h <= 0 ) return ;
800+
801+ const dt_imgid_t imgid = darktable .develop -> image_storage .id ;
802+ if (!dt_is_valid_imgid (imgid )) return ;
803+
804+ // get the raster mask root folder from preferences
805+ gchar * root = dt_conf_get_string ("plugins/darkroom/segments/def_path" );
806+ if (!root || !* root )
807+ {
808+ g_free (root );
809+ dt_control_log (_ ("set raster mask root folder in preferences" ));
810+ return ;
811+ }
812+
813+ // ensure the directory exists
814+ if (g_mkdir_with_parents (root , 0755 ) != 0 )
815+ {
816+ dt_print (DT_DEBUG_AI , "[object mask] cannot create folder: %s" , root );
817+ dt_control_log (_ ("cannot create raster mask folder" ));
818+ g_free (root );
819+ return ;
820+ }
821+
822+ // get image filename without directory and extension
823+ char imgpath [PATH_MAX ] = { 0 };
824+ dt_image_full_path (imgid , imgpath , sizeof (imgpath ), NULL );
825+ gchar * basename = g_path_get_basename (imgpath );
826+ char * dot = g_strrstr (basename , "." );
827+ if (dot ) * dot = '\0' ;
828+
829+ // build output path, append _1, _2, ... if file already exists
830+ gchar * mask_name = g_strdup_printf ("%s_mask.png" , basename );
831+ gchar * outpath = g_build_filename (root , mask_name , NULL );
832+ g_free (mask_name );
833+
834+ for (int seq = 1 ;
835+ g_file_test (outpath , G_FILE_TEST_EXISTS ) && seq < 1000 ;
836+ seq ++ )
837+ {
838+ g_free (outpath );
839+ mask_name = g_strdup_printf ("%s_mask_%d.png" , basename , seq );
840+ outpath = g_build_filename (root , mask_name , NULL );
841+ g_free (mask_name );
842+ }
843+
844+ g_free (basename );
845+ g_free (root );
846+
847+ // create RGB buffer (rasterfile module expects 3-channel PNG)
848+ const int stride = cairo_format_stride_for_width (CAIRO_FORMAT_RGB24 , w );
849+ uint8_t * buf = g_try_malloc0 ((size_t )stride * h );
850+ if (!buf )
851+ {
852+ g_free (outpath );
853+ return ;
854+ }
855+
856+ for (int y = 0 ; y < h ; y ++ )
857+ {
858+ uint8_t * row = buf + y * stride ;
859+ for (int x = 0 ; x < w ; x ++ )
860+ {
861+ const uint8_t v = (mask [y * w + x ] > threshold ) ? 255 : 0 ;
862+ // cairo RGB24 is native-endian BGRX in memory
863+ row [x * 4 + 0 ] = v ; // B
864+ row [x * 4 + 1 ] = v ; // G
865+ row [x * 4 + 2 ] = v ; // R
866+ row [x * 4 + 3 ] = 0 ; // unused
867+ }
868+ }
869+
870+ cairo_surface_t * surface
871+ = cairo_image_surface_create_for_data (buf , CAIRO_FORMAT_RGB24 ,
872+ w , h , stride );
873+ if (surface )
874+ {
875+ const cairo_status_t st = cairo_surface_write_to_png (surface , outpath );
876+ cairo_surface_destroy (surface );
877+ if (st == CAIRO_STATUS_SUCCESS )
878+ {
879+ dt_print (DT_DEBUG_AI , "[object mask] raster mask saved: %s" , outpath );
880+ dt_control_log (_ ("raster mask saved" ));
881+ }
882+ else
883+ {
884+ dt_print (DT_DEBUG_AI , "[object mask] failed to write: %s" , outpath );
885+ dt_control_log (_ ("failed to save raster mask" ));
886+ }
887+ }
888+ g_free (buf );
889+ g_free (outpath );
890+ }
891+
792892// transform mask-space forms to input-normalized coords and register them,
793893// takes ownership of `forms` and `signs` lists (forms are appended to dev->forms)
794894static dt_masks_form_t *
@@ -1139,6 +1239,15 @@ static int _object_events_button_pressed(
11391239 if (d && g_atomic_int_get (& d -> encode_state ) == ENCODE_RUNNING )
11401240 return 1 ;
11411241
1242+ // shift+right-click: save raster mask before vectorization
1243+ if (d && d -> has_selection && d -> mask
1244+ && dt_modifier_is (state , GDK_SHIFT_MASK ))
1245+ {
1246+ const float thresh = CLAMP (
1247+ dt_conf_get_float (CONF_OBJECT_THRESHOLD_KEY ), 0.3f , 0.9f );
1248+ _save_raster_mask (d -> mask , d -> mask_w , d -> mask_h , thresh );
1249+ }
1250+
11421251 // right-click: finalize mask (prefer cached preview forms)
11431252 dt_masks_form_t * new_grp = NULL ;
11441253 if (d && d -> preview_forms )
@@ -1835,8 +1944,11 @@ static void _object_set_hint_message(
18351944 g_snprintf (msgbuf ,
18361945 msgbuf_len ,
18371946 _ ("<b>add</b>: click, <b>subtract</b>: shift+click, "
1838- "<b>clear</b>: ctrl+shift+click, <b>apply</b>: right-click\n"
1839- "<b>smoothing</b>: scroll (%3.2f), <b>cleanup</b>: shift+scroll (%d), "
1947+ "<b>clear</b>: ctrl+shift+click\n"
1948+ "<b>apply</b>: right-click, "
1949+ "<b>apply+save raster</b>: shift+right-click\n"
1950+ "<b>smoothing</b>: scroll (%3.2f), "
1951+ "<b>cleanup</b>: shift+scroll (%d), "
18401952 "<b>opacity</b>: ctrl+scroll (%d%%)" ),
18411953 d -> preview_smoothing , d -> preview_cleanup , opacity );
18421954 else
0 commit comments