The latest scanner settings are now automatically saved in the HKEY_CURRENT_USER\Software\ScannersSettings\Vendor\Product path. This modification was tested and worked correctly.
-- v2: sane.ds: Adding saving scanner settings to the registry
From: Ivan Lyugaev valy@etersoft.ru
The latest scanner settings are now automatically saved in the HKEY_CURRENT_USER\Software\ScannersSettings<Vendor><Product> path --- dlls/sane.ds/Makefile.in | 5 +- dlls/sane.ds/cfg.c | 148 ++++++++++++++++++++++++ dlls/sane.ds/cfg.h | 48 ++++++++ dlls/sane.ds/sane_main.c | 3 + dlls/sane.ds/ui.c | 242 ++++++++++++++++++++++++++++++++++++--- dlls/sane.ds/unixlib.c | 3 + dlls/sane.ds/unixlib.h | 1 + 7 files changed, 433 insertions(+), 17 deletions(-) create mode 100644 dlls/sane.ds/cfg.c create mode 100644 dlls/sane.ds/cfg.h
diff --git a/dlls/sane.ds/Makefile.in b/dlls/sane.ds/Makefile.in index eed1acdf918..28549e75740 100644 --- a/dlls/sane.ds/Makefile.in +++ b/dlls/sane.ds/Makefile.in @@ -1,6 +1,6 @@ MODULE = sane.ds UNIXLIB = sane.so -IMPORTS = comctl32 user32 gdi32 +IMPORTS = comctl32 user32 gdi32 kernelbase UNIX_LIBS = $(SANE_LIBS) UNIX_CFLAGS = $(SANE_CFLAGS)
@@ -12,4 +12,5 @@ SOURCES = \ sane.rc \ sane_main.c \ ui.c \ - unixlib.c + unixlib.c \ + cfg.c diff --git a/dlls/sane.ds/cfg.c b/dlls/sane.ds/cfg.c new file mode 100644 index 00000000000..cbbdf834298 --- /dev/null +++ b/dlls/sane.ds/cfg.c @@ -0,0 +1,148 @@ +/* +* TWAIN32 Configuration Manager +* +* Copyright 2025 Ivan Lyugaev +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA +*/ + +#include <stdio.h> +#include <stdlib.h> +#include "wine/debug.h" + +#include "cfg.h" + +WINE_DEFAULT_DEBUG_CHANNEL( twain ); + +LSTATUS get_info_key( WCHAR* path, HKEY* h_key, DWORD* dispos ) +{ + WCHAR reg_path[MAX_PATH]; + swprintf( reg_path, MAX_PATH, L"Software\ScannersSettings\%s", path ); + + return RegCreateKeyExW( + HKEY_CURRENT_USER, + reg_path, + 0, + NULL, + REG_OPTION_NON_VOLATILE, + KEY_ALL_ACCESS, + NULL, + h_key, + dispos + ); +} + +BOOL is_exist_reg( WCHAR* path ) +{ + HKEY h_key; + DWORD dispos; + LSTATUS res; + + res = get_info_key(path, &h_key, &dispos); + + if ( res != ERROR_SUCCESS ) + { + ERR( "RegCreateKeyExW error: %ld\n", res ); + return FALSE; + } + + RegCloseKey(h_key); + return ( dispos == REG_OPENED_EXISTING_KEY ); +} + +BOOL save_to_reg( WCHAR* path, DWORD reg_type, CHAR* name, const BYTE* value, DWORD size ) +{ + HKEY h_key; + DWORD dispos; + LSTATUS res; + + res = get_info_key(path, &h_key, &dispos); + + if( res != ERROR_SUCCESS ) + { + ERR( "RegCreateKeyExW: %ld\n", res ); + return FALSE; + } + + res = RegSetValueExA( + h_key, + name, + 0, + reg_type, + value, + size + ); + + RegCloseKey( h_key ); + + if ( res != ERROR_SUCCESS ) { + ERR( "RegSetValueExA error: %ld\n", res ); + return FALSE; + } + + return TRUE; +} + +BOOL load_from_reg( WCHAR* path, int opt_type, CHAR* name, void* value ) +{ + HKEY h_key; + DWORD dispos, flag, size; + LSTATUS res; + + res = get_info_key(path, &h_key, &dispos); + + if( res != ERROR_SUCCESS ) + { + ERR( "RegCreateKeyExW: %ld\n", res ); + return FALSE; + } + + switch( opt_type ) + { + case TYPE_INT: + case TYPE_FIXED: + case TYPE_BOOL: + flag = RRF_RT_REG_DWORD; + size = sizeof(DWORD); + break; + case TYPE_STRING: + flag = RRF_RT_REG_SZ; + size = OPTION_VALUE_MAX; + break; + default: + RegCloseKey( h_key ); + ERR( "Unknown type: %d\n", opt_type ); + return FALSE; + } + + res = RegGetValueA( + h_key, + NULL, + name, + flag, + NULL, + value, + &size + ); + + RegCloseKey( h_key ); + + if ( res != ERROR_SUCCESS ) { + ERR( "RegGetValueExA error: %ld\n", res ); + return FALSE; + } + + return TRUE; +} diff --git a/dlls/sane.ds/cfg.h b/dlls/sane.ds/cfg.h new file mode 100644 index 00000000000..9bea843deef --- /dev/null +++ b/dlls/sane.ds/cfg.h @@ -0,0 +1,48 @@ +/* +* TWAIN32 Configuration Manager +* +* Copyright 2025 Ivan Lyugaev +* +* This library is free software; you can redistribute it and/or +* modify it under the terms of the GNU Lesser General Public +* License as published by the Free Software Foundation; either +* version 2.1 of the License, or (at your option) any later version. +* +* This library is distributed in the hope that it will be useful, +* but WITHOUT ANY WARRANTY; without even the implied warranty of +* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +* Lesser General Public License for more details. +* +* You should have received a copy of the GNU Lesser General Public +* License along with this library; if not, write to the Free Software +* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA +*/ + +#include <stdlib.h> +#include <stdarg.h> +#include <stdio.h> + +#include "windef.h" +#include "winbase.h" +#include "winreg.h" + +#include "sane_i.h" + +#define OPTION_VALUE_MAX 255 +#define OPTION_NAME_MAX 64 + +typedef struct +{ + INT opt_type; + DWORD reg_type; + CHAR name[OPTION_NAME_MAX]; + INT optno; + BYTE value[OPTION_VALUE_MAX]; + DWORD size; + BOOL is_enabled; +} ScannerOption; + +BOOL save_to_reg( WCHAR* path, DWORD reg_type, CHAR* name, const BYTE* value, DWORD size ); +BOOL load_from_reg( WCHAR* path, int opt_type, CHAR* name, void* value ); + +BOOL is_exist_reg( WCHAR* path ); diff --git a/dlls/sane.ds/sane_main.c b/dlls/sane.ds/sane_main.c index 8f07ce6d52a..360509e2bf6 100644 --- a/dlls/sane.ds/sane_main.c +++ b/dlls/sane.ds/sane_main.c @@ -74,6 +74,9 @@ static TW_UINT16 SANE_OpenDS( pTW_IDENTITY pOrigin, pTW_IDENTITY self) activeDS.twCC = SANE_SaneSetDefaults(); if (activeDS.twCC == TWCC_SUCCESS) { + strcpy(activeDS.identity.Manufacturer, self->Manufacturer); + strcpy(activeDS.identity.ProductFamily, self->ProductFamily); + strcpy(activeDS.identity.ProductName, self->ProductName); activeDS.currentState = 4; activeDS.identity.Id = self->Id; activeDS.appIdentity = *pOrigin; diff --git a/dlls/sane.ds/ui.c b/dlls/sane.ds/ui.c index 25a1cf33970..a41f1bd9bfd 100644 --- a/dlls/sane.ds/ui.c +++ b/dlls/sane.ds/ui.c @@ -30,11 +30,16 @@ #include "wine/debug.h" #include "resource.h"
+#include "cfg.h" + WINE_DEFAULT_DEBUG_CHANNEL(twain);
-#define ID_BASE 0x100 -#define ID_EDIT_BASE 0x1000 -#define ID_STATIC_BASE 0x2000 +#define ID_BASE 0x100 +#define ID_EDIT_BASE 0x1000 +#define ID_STATIC_BASE 0x2000 + +WCHAR path[MAX_PATH]; +int gOptCount;
static INT_PTR CALLBACK DialogProc (HWND , UINT , WPARAM , LPARAM ); static INT CALLBACK PropSheetProc(HWND, UINT,LPARAM); @@ -485,6 +490,8 @@ BOOL DoScannerUI(void)
hdc = CreateCompatibleDC(0);
+ gOptCount = optcount; + while (index < optcount) { struct option_descriptor opt; @@ -508,13 +515,19 @@ BOOL DoScannerUI(void) psp[page_count].lParam = (LPARAM)&activeDS; page_count ++; } - + index ++; } - + len = lstrlenA(activeDS.identity.Manufacturer) + lstrlenA(activeDS.identity.ProductName) + 2; szCaption = malloc(len *sizeof(WCHAR)); + + swprintf(path, MAX_PATH, L"%S\%S_%S.sc", + activeDS.identity.Manufacturer, + activeDS.identity.ProductFamily, + activeDS.identity.ProductName); + MultiByteToWideChar(CP_ACP,0,activeDS.identity.Manufacturer,-1, szCaption,len); szCaption[lstrlenA(activeDS.identity.Manufacturer)] = ' '; @@ -548,6 +561,115 @@ BOOL DoScannerUI(void) return FALSE; }
+static void get_option(struct option_descriptor* opt, ScannerOption* option) +{ + lstrcpynA(option->name, opt->name, OPTION_NAME_MAX); + option->is_enabled = opt->is_active; + option->optno = opt->optno; + + if (opt->type ==TYPE_STRING && opt->constraint_type != CONSTRAINT_NONE) + { + CHAR buffer[255]; + option->reg_type = REG_SZ; + option->opt_type = opt->type; + sane_option_get_value(opt->optno, buffer); + lstrcpynA((CHAR*)option->value, buffer, OPTION_VALUE_MAX); + option->size = (DWORD)(strlen(buffer) + 1); + } + else if (opt->type == TYPE_BOOL) + { + BOOL b; + option->opt_type = opt->type; + option->reg_type = REG_DWORD; + sane_option_get_value(opt->optno, &b); + memcpy(option->value, &b, sizeof(BOOL)); + option->size = sizeof(b); + } + else if (opt->type == TYPE_INT && opt->constraint_type == CONSTRAINT_WORD_LIST) + { + int val; + option->opt_type = opt->type; + option->reg_type = REG_DWORD; + sane_option_get_value(opt->optno, &val); + memcpy(option->value, &val, sizeof(INT)); + option->size = sizeof(val); + } + else if (opt->constraint_type == CONSTRAINT_RANGE) + { + if (opt->type == TYPE_INT) + { + int si; + option->opt_type = opt->type; + option->reg_type = REG_DWORD; + sane_option_get_value(opt->optno, &si); + if (opt->constraint.range.quant) + { + si = si / opt->constraint.range.quant; + } + memcpy(option->value, &si, sizeof(INT)); + option->size = sizeof(si); + } + else if (opt->type == TYPE_FIXED) + { + int pos, *sf; + option->opt_type = opt->type; + option->reg_type = REG_DWORD; + sf = calloc( opt->size, sizeof(int) ); + sane_option_get_value(opt->optno, sf ); + if (opt->constraint.range.quant) + pos = *sf / opt->constraint.range.quant; + else + pos = MulDiv( *sf, 100, 65536 ); + memcpy(option->value, &pos, sizeof(INT)); + option->size = sizeof(pos); + free(sf); + } + } +} + +static BOOL save_option(int optno) +{ + ScannerOption option; + struct option_descriptor opt; + + opt.optno = optno; + SANE_CALL( option_get_descriptor, &opt); + + get_option(&opt, &option); + return save_to_reg(path, option.reg_type, option.name, option.value, option.size); +} + +static BOOL save_options(void) +{ + TW_UINT16 rc; + int optcount; + + rc = sane_option_get_value( 0, &optcount ); + if (rc != TWCC_SUCCESS) + { + ERR("Unable to read number of options\n"); + optcount = gOptCount; + } + else + gOptCount = optcount; + + for (int i = 1; i < optcount; i++) + { + ScannerOption option; + struct option_descriptor opt; + opt.optno = i; + SANE_CALL( option_get_descriptor, &opt); + if (!opt.is_active) + continue; + get_option(&opt, &option); + if (!save_to_reg(path, option.reg_type, option.name, option.value, option.size)) + { + ERR("Option %s could not be saved to the registry!", option.name); + } + } + return TRUE; +} + static void UpdateRelevantEdit(HWND hwnd, const struct option_descriptor *opt, int position) { WCHAR buffer[244]; @@ -607,6 +729,7 @@ static BOOL UpdateSaneScrollOption(const struct option_descriptor *opt, DWORD po si = position;
sane_option_set_value( opt->optno, &si, &result ); + save_option(opt->optno); break; } case TYPE_FIXED: @@ -616,6 +739,7 @@ static BOOL UpdateSaneScrollOption(const struct option_descriptor *opt, DWORD po si = MulDiv( position, 65536, 100 );
sane_option_set_value( opt->optno, &si, &result ); + save_option(opt->optno); break; default: break; @@ -635,19 +759,27 @@ static INT_PTR InitializeDialog(HWND hwnd) if (rc != TWCC_SUCCESS) { ERR("Unable to read number of options\n"); - return FALSE; + optcount = gOptCount; + } + else + gOptCount = optcount; + + if ( !is_exist_reg(path) ) + { + save_options(); }
for ( i = 1; i < optcount; i++) { + CHAR title[256]; struct option_descriptor opt; - control = GetDlgItem(hwnd,i+ID_BASE);
if (!control) continue;
opt.optno = i; + SANE_CALL( option_get_descriptor, &opt );
TRACE("%i %s %i %i\n",i,debugstr_w(opt.title),opt.type,opt.constraint_type); @@ -655,34 +787,77 @@ static INT_PTR InitializeDialog(HWND hwnd)
SendMessageA(control,CB_RESETCONTENT,0,0); /* initialize values */ + + lstrcpynA(title, opt.name, ARRAY_SIZE(title)); + if (opt.type == TYPE_STRING && opt.constraint_type != CONSTRAINT_NONE) { CHAR buffer[255]; WCHAR *p;
+ BOOL is_exist = load_from_reg(path, opt.type, title, buffer); + BOOL is_correct = FALSE; + for (p = opt.constraint.strings; *p; p += lstrlenW(p) + 1) + { + CHAR param[256]; SendMessageW( control,CB_ADDSTRING,0, (LPARAM)p ); + WideCharToMultiByte(CP_UTF8, 0, p, -1, param, sizeof(param), NULL, NULL); + if (is_exist && !strcmp(param, buffer)) + { + is_correct = TRUE; + } + } + + if (is_exist && is_correct) + { + sane_option_set_value(opt.optno, buffer, NULL); + } + + if (is_exist && !is_correct) + { + ERR("%s=%s is incorrect. The default value is set!", title, buffer); + } + sane_option_get_value( i, buffer ); SendMessageA(control,CB_SELECTSTRING,0,(LPARAM)buffer); } else if (opt.type == TYPE_BOOL) { BOOL b; - sane_option_get_value( i, &b ); - if (b) - SendMessageA(control,BM_SETCHECK,BST_CHECKED,0); + BOOL is_exist = load_from_reg(path, opt.type, title, &b);
+ if (is_exist) + { + sane_option_set_value( i, &b, NULL ); + } + + sane_option_get_value( i, &b ); + SendMessageA(control,BM_SETCHECK, b ? BST_CHECKED : BST_UNCHECKED,0); } else if (opt.type == TYPE_INT && opt.constraint_type == CONSTRAINT_WORD_LIST) { int j, count = opt.constraint.word_list[0]; CHAR buffer[16]; int val; + BOOL is_exist = load_from_reg(path, opt.type, title, &val); + BOOL is_correct = FALSE; + for (j=1; j<=count; j++) { + if (opt.constraint.word_list[j] == val) + { + is_correct = TRUE; + } sprintf(buffer, "%d", opt.constraint.word_list[j]); SendMessageA(control, CB_ADDSTRING, 0, (LPARAM)buffer); } + if (is_exist && is_correct) + sane_option_set_value( i, &val, NULL ); + + if (is_exist && !is_correct) + ERR("%s=%d is incorrect. The default value is set!\n", title, val); + sane_option_get_value( i, &val ); sprintf(buffer, "%d", val); SendMessageA(control,CB_SELECTSTRING,0,(LPARAM)buffer); @@ -693,6 +868,8 @@ static INT_PTR InitializeDialog(HWND hwnd) { int si; int min,max; + BOOL is_exist = load_from_reg(path, opt.type, title, &si); + BOOL is_correct = FALSE;
min = opt.constraint.range.min / (opt.constraint.range.quant ? opt.constraint.range.quant : 1); @@ -702,7 +879,16 @@ static INT_PTR InitializeDialog(HWND hwnd)
SendMessageA(control,SBM_SETRANGE,min,max);
+ if (is_exist && si >= min && si <= max) + is_correct = TRUE; + else if (is_exist) + ERR("%s=%d is out of range [%d..%d]. The default value is used!\n", title, si, min, max); + + if (is_correct && is_exist) + sane_option_set_value( i, &si, NULL); + sane_option_get_value( i, &si ); + if (opt.constraint.range.quant) si = si / opt.constraint.range.quant;
@@ -711,8 +897,8 @@ static INT_PTR InitializeDialog(HWND hwnd) } else if (opt.type == TYPE_FIXED) { - int pos, min, max, *sf; - + int pos, min, max, *sf, val; + BOOL is_exist, is_correct = FALSE; if (opt.constraint.range.quant) { min = opt.constraint.range.min / opt.constraint.range.quant; @@ -726,6 +912,23 @@ static INT_PTR InitializeDialog(HWND hwnd)
SendMessageA(control,SBM_SETRANGE,min,max);
+ is_exist = load_from_reg(path, opt.type, title, &val); + + if (is_exist && val >= min && val <= max) + is_correct = TRUE; + else if (is_exist) + ERR("%s = %d is out of range [%d..%d]. The default value is used!\n", title, val, min, max); + + if (is_exist && is_correct) + { + int valSet; + if (opt.constraint.range.quant) + valSet = val * opt.constraint.range.quant; + else + valSet = MulDiv(val, 65536, 100); + sane_option_set_value(i, &valSet, NULL); + } +
sf = calloc( opt.size, sizeof(int) ); sane_option_get_value( i, sf ); @@ -817,7 +1020,12 @@ static void ButtonClicked(HWND hwnd, INT id, HWND control) { BOOL r = SendMessageW(control,BM_GETCHECK,0,0)==BST_CHECKED; sane_option_set_value( opt.optno, &r, &changed ); - if (changed) InitializeDialog(hwnd); + + if (changed) + { + save_option(opt.optno); + InitializeDialog(hwnd); + } } }
@@ -851,11 +1059,15 @@ static void ComboChanged(HWND hwnd, INT id, HWND control) int val = atoi( value ); sane_option_set_value( opt.optno, &val, &changed ); } - if (changed) InitializeDialog(hwnd); + + if (changed) + { + save_option(opt.optno); + InitializeDialog(hwnd); + } free( value ); }
- static INT_PTR CALLBACK DialogProc(HWND hwndDlg, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) diff --git a/dlls/sane.ds/unixlib.c b/dlls/sane.ds/unixlib.c index 69f085450a8..0ec443f786b 100644 --- a/dlls/sane.ds/unixlib.c +++ b/dlls/sane.ds/unixlib.c @@ -160,9 +160,12 @@ static void map_descr( struct option_descriptor *descr, const SANE_Option_Descri descr->size = opt->size; descr->is_active = SANE_OPTION_IS_ACTIVE( opt->cap ); descr->is_settable = SANE_OPTION_IS_SETTABLE( opt->cap ); + if (opt->title) len = ntdll_umbstowcs( opt->title, strlen(opt->title), descr->title, ARRAY_SIZE(descr->title) ); descr->title[len] = 0; + if (opt->name) lstrcpynA(descr->name, opt->name, ARRAY_SIZE(descr->name)); + switch (descr->constraint_type) { case CONSTRAINT_RANGE: diff --git a/dlls/sane.ds/unixlib.h b/dlls/sane.ds/unixlib.h index 34e7a9a9796..7d817c89785 100644 --- a/dlls/sane.ds/unixlib.h +++ b/dlls/sane.ds/unixlib.h @@ -45,6 +45,7 @@ struct option_descriptor enum { CONSTRAINT_NONE, CONSTRAINT_RANGE, CONSTRAINT_WORD_LIST, CONSTRAINT_STRING_LIST } constraint_type;
WCHAR title[256]; + CHAR name[256];
union {
On Wed Sep 3 20:08:30 2025 +0000, Ivan Lyugaev wrote:
This block repeats the initialization of the parameters in the InitializeDialog function. I have saved only the necessary operations to generate the parameters. ` else if (opt.type == TYPE_FIXED) { int pos, min, max, *sf; if (opt.constraint.range.quant) { min = opt.constraint.range.min / opt.constraint.range.quant; max = opt.constraint.range.max / opt.constraint.range.quant; } else { min = MulDiv( opt.constraint.range.min, 100, 65536 ); max = MulDiv( opt.constraint.range.max, 100, 65536 ); } SendMessageA(control,SBM_SETRANGE,min,max); sf = calloc( opt.size, sizeof(int) ); sane_option_get_value( i, sf ); /* Note that conversion of float -> SANE_Fixed is lossy; * and when you truncate it into an integer, you can get * unfortunate results. This calculation attempts * to mitigate that harm */ if (opt.constraint.range.quant) pos = *sf / opt.constraint.range.quant; else pos = MulDiv( *sf, 100, 65536 ); free(sf); SendMessageW(control, SBM_SETPOS, pos, TRUE); UpdateRelevantEdit(hwnd, &opt, pos); } `
If a case isn't covered in InitializeDialog, the value just isn't set in the UI. If a case isn't covered here, undefined memory is used.
On Wed Sep 3 20:18:11 2025 +0000, Ivan Lyugaev wrote:
This block repeats the initialization of the parameters in the InitializeDialog function. I have saved only the necessary operations to generate the parameters. ` /* initialize values */ if (opt.type == TYPE_STRING && opt.constraint_type != CONSTRAINT_NONE) { CHAR buffer[255]; WCHAR *p; for (p = opt.constraint.strings; *p; p += lstrlenW(p) + 1) SendMessageW( control,CB_ADDSTRING,0, (LPARAM)p ); sane_option_get_value( i, buffer ); SendMessageA(control,CB_SELECTSTRING,0,(LPARAM)buffer); } `
TYPE_STRING with CONSTRAINT_NONE is handled in `create_item`. It seems that an editbox is created in this case. I guess it's assumed that there is no default value that we would need to set, and the only way it would change is by user input.
But since it's not handled in InitializeDialog, handling it here wouldn't make it possible to load the setting. I think that's OK for an initial implementation, but we should make sure undefined memory isn't used later, and print some error so it's possible to tell what's wrong.
On Wed Sep 3 20:23:11 2025 +0000, Ivan Lyugaev wrote:
You're right, the printer initially uses the default settings (sane settings). But it is important to note here that this fragment is necessary at the first contact with the scanner for the initial initialization of the registry. If the registry key does not exist, it is first initialized with the current (default) settings, and then changes are made to it.
I don't think it is necessary to create the key here because `save_to_reg` will already create it as needed, and any settings missing from the registry will mean we leave the default in place.
If it were necessary, we would have a problem, because there is a possibility of a registry key being created without all the settings (if for some reason the process is interrupted), or new settings being added later.