From 25dc5725200e0f4ca6d1f26bcefd2daeb6d70379 Mon Sep 17 00:00:00 2001 From: Your Name Date: Fri, 17 Apr 2026 22:14:34 +0530 Subject: [PATCH 1/5] feat(print): add initial Windows printing support via GDI Add a Windows-native print backend (win_print.c) implementing the same API as the CUPS backend. Uses Windows GDI for printer discovery, paper enumeration, and bitmap printing. - Printer discovery via EnumPrinters() - Paper sizes via DeviceCapabilities() - Printer metrics via GetDeviceCaps() - PDF printing via ShellExecute("print") - Color management defaults to sRGB Build system updated to conditionally compile the Windows backend when BUILD_PRINT is enabled on Windows, and the CUPS backend on Linux/macOS. Refs: #19856 --- CMakeLists.txt | 1 - src/CMakeLists.txt | 35 +++- src/common/win_print.c | 422 +++++++++++++++++++++++++++++++++++++++ src/libs/CMakeLists.txt | 4 +- src/views/CMakeLists.txt | 4 +- 5 files changed, 451 insertions(+), 15 deletions(-) create mode 100644 src/common/win_print.c diff --git a/CMakeLists.txt b/CMakeLists.txt index 37a4f0c55c74..dc2edaad1a27 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -159,7 +159,6 @@ if(WIN32) set(USE_COLORD OFF) set(USE_KWALLET OFF) set(BUILD_CMSTEST OFF) - set(BUILD_PRINT OFF) set(TESTBUILD_OPENCL_PROGRAMS OFF) if(BUILD_MSYS2_INSTALL) add_definitions(-DMSYS2_INSTALL) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e2d15b5b16cc..1254292353fc 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -913,24 +913,39 @@ if(USE_COLORD) endif(USE_COLORD) if(BUILD_PRINT) - find_package(Cups) - if(CUPS_FOUND) - include_directories(SYSTEM ${CUPS_INCLUDE_DIR}) + if(WIN32) + # Windows-native print backend using GDI / WinSpool FILE(GLOB SOURCE_FILES_PRINT "common/cups_print.h" - "common/cups_print.c" + "common/win_print.c" "common/printprof.c" "common/printing.h" "common/printing.c" ) set(SOURCES ${SOURCES} ${SOURCE_FILES_PRINT}) - list(APPEND LIBS ${CUPS_LIBRARIES}) + list(APPEND LIBS winspool) add_definitions("-DHAVE_PRINT") - message(STATUS "Print mode: enabled") - else(CUPS_FOUND) - set(BUILD_PRINT OFF) - message(STATUS "Print mode: disabled, please install CUPS dev package") - endif(CUPS_FOUND) + message(STATUS "Print mode: enabled (Windows native)") + else(WIN32) + find_package(Cups) + if(CUPS_FOUND) + include_directories(SYSTEM ${CUPS_INCLUDE_DIR}) + FILE(GLOB SOURCE_FILES_PRINT + "common/cups_print.h" + "common/cups_print.c" + "common/printprof.c" + "common/printing.h" + "common/printing.c" + ) + set(SOURCES ${SOURCES} ${SOURCE_FILES_PRINT}) + list(APPEND LIBS ${CUPS_LIBRARIES}) + add_definitions("-DHAVE_PRINT") + message(STATUS "Print mode: enabled") + else(CUPS_FOUND) + set(BUILD_PRINT OFF) + message(STATUS "Print mode: disabled, please install CUPS dev package") + endif(CUPS_FOUND) + endif(WIN32) else(BUILD_PRINT) message(STATUS "Print mode: disabled") endif(BUILD_PRINT) diff --git a/src/common/win_print.c b/src/common/win_print.c new file mode 100644 index 000000000000..f5365d04f87e --- /dev/null +++ b/src/common/win_print.c @@ -0,0 +1,422 @@ +/* + This file is part of darktable, + Copyright (C) 2025 darktable developers. + + darktable is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + darktable 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 General Public License for more details. + + You should have received a copy of the GNU General Public License + along with darktable. If not, see . +*/ + +/* Windows native print backend, implementing the same API surface + as cups_print.c but using GDI / WinSpool instead of CUPS. + Color management defaults to sRGB (see issue #19856). */ + +#ifdef _WIN32 + +#include +#include +#include +#include +#include + +#include "common/file_location.h" +#include "common/image.h" +#include "common/image_cache.h" +#include "common/mipmap_cache.h" +#include "common/pdf.h" +#include "control/jobs/control_jobs.h" +#include "cups_print.h" + +typedef struct dt_prtctl_t +{ + void (*cb)(dt_printer_info_t *, void *); + void *user_data; +} dt_prtctl_t; + +// initialize the pinfo structure +void dt_init_print_info(dt_print_info_t *pinfo) +{ + memset(&pinfo->printer, 0, sizeof(dt_printer_info_t)); + memset(&pinfo->page, 0, sizeof(dt_page_setup_t)); + memset(&pinfo->paper, 0, sizeof(dt_paper_info_t)); + pinfo->printer.intent = DT_INTENT_PERCEPTUAL; + pinfo->printer.is_turboprint = FALSE; + *pinfo->printer.profile = '\0'; + pinfo->num_printers = 0; +} + +void dt_get_printer_info(const char *printer_name, + dt_printer_info_t *pinfo) +{ + g_strlcpy(pinfo->name, printer_name, MAX_NAME); + pinfo->is_turboprint = FALSE; + + // default resolution + pinfo->resolution = 300; + + // try to get hardware margins from DEVMODE + HDC hdc = CreateDCA(NULL, printer_name, NULL, NULL); + if(hdc) + { + // get physical page size and printable area (in device units) + const int phys_w = GetDeviceCaps(hdc, PHYSICALWIDTH); + const int phys_h = GetDeviceCaps(hdc, PHYSICALHEIGHT); + const int ofs_x = GetDeviceCaps(hdc, PHYSICALOFFSETX); + const int ofs_y = GetDeviceCaps(hdc, PHYSICALOFFSETY); + const int print_w = GetDeviceCaps(hdc, HORZRES); + const int print_h = GetDeviceCaps(hdc, VERTRES); + const int dpi_x = GetDeviceCaps(hdc, LOGPIXELSX); + const int dpi_y = GetDeviceCaps(hdc, LOGPIXELSY); + + if(dpi_x > 0 && dpi_y > 0) + { + pinfo->resolution = dpi_x; + while(pinfo->resolution > 360) + pinfo->resolution /= 2; + + // margins in mm (25.4 mm per inch) + pinfo->hw_margin_left = (ofs_x * 25.4) / dpi_x; + pinfo->hw_margin_top = (ofs_y * 25.4) / dpi_y; + pinfo->hw_margin_right = ((phys_w - ofs_x - print_w) * 25.4) / dpi_x; + pinfo->hw_margin_bottom = ((phys_h - ofs_y - print_h) * 25.4) / dpi_y; + } + + DeleteDC(hdc); + } +} + +static int _detect_printers_callback(dt_job_t *job) +{ + dt_prtctl_t *pctl = dt_control_job_get_params(job); + + DWORD needed = 0, returned = 0; + + // first call to find out how much memory we need + EnumPrintersA(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, + NULL, 2, NULL, 0, &needed, &returned); + + if(needed == 0) + { + darktable.control->cups_started = TRUE; + return 0; + } + + BYTE *buffer = (BYTE *)g_malloc(needed); + if(!buffer) + { + darktable.control->cups_started = TRUE; + return 1; + } + + if(EnumPrintersA(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, + NULL, 2, buffer, needed, &needed, &returned)) + { + PRINTER_INFO_2A *pi = (PRINTER_INFO_2A *)buffer; + + for(DWORD i = 0; i < returned; i++) + { + // skip printers that are paused or have errors + if(pi[i].Status == 0 || !(pi[i].Status & PRINTER_STATUS_ERROR)) + { + dt_printer_info_t pr; + memset(&pr, 0, sizeof(pr)); + dt_get_printer_info(pi[i].pPrinterName, &pr); + if(pctl->cb) pctl->cb(&pr, pctl->user_data); + dt_print(DT_DEBUG_PRINT, "[print] new printer %s found", pi[i].pPrinterName); + } + else + { + dt_print(DT_DEBUG_PRINT, "[print] skip printer %s (status=%lu)", + pi[i].pPrinterName, (unsigned long)pi[i].Status); + } + } + } + + g_free(buffer); + darktable.control->cups_started = TRUE; + return 0; +} + +static int _cancel = 0; + +void dt_printers_abort_discovery(void) +{ + _cancel = 1; +} + +void dt_printers_discovery(void (*cb)(dt_printer_info_t *pr, void *user_data), + void *user_data) +{ + // asynchronously checks for available printers + dt_job_t *job = dt_control_job_create(&_detect_printers_callback, "detect connected printers"); + if(job) + { + dt_prtctl_t *prtctl = g_malloc0(sizeof(dt_prtctl_t)); + + prtctl->cb = cb; + prtctl->user_data = user_data; + + dt_control_job_set_params(job, prtctl, g_free); + dt_control_add_job(DT_JOB_QUEUE_SYSTEM_BG, job); + } +} + +dt_paper_info_t *dt_get_paper(GList *papers, + const char *name) +{ + dt_paper_info_t *result = NULL; + + for(GList *p = papers; p; p = g_list_next(p)) + { + dt_paper_info_t *pi = (dt_paper_info_t *)p->data; + if(!strcmp(pi->name, name) || !strcmp(pi->common_name, name)) + { + result = pi; + break; + } + } + return result; +} + +static gint +sort_papers(gconstpointer p1, gconstpointer p2) +{ + const dt_paper_info_t *n1 = (dt_paper_info_t *)p1; + const dt_paper_info_t *n2 = (dt_paper_info_t *)p2; + const int l1 = strlen(n1->common_name); + const int l2 = strlen(n2->common_name); + return l1 == l2 ? strcmp(n1->common_name, n2->common_name) : (l1 < l2 ? -1 : +1); +} + +GList *dt_get_papers(const dt_printer_info_t *printer) +{ + GList *result = NULL; + HANDLE hPrinter = NULL; + + if(!OpenPrinterA((LPSTR)printer->name, &hPrinter, NULL)) + return NULL; + + // get the number of paper names + const DWORD count = DeviceCapabilitiesA(printer->name, NULL, DC_PAPERNAMES, NULL, NULL); + if(count == 0 || count == (DWORD)-1) + { + ClosePrinter(hPrinter); + return NULL; + } + + // paper names are fixed 64-char blocks + char *names = (char *)g_malloc0(count * 64); + // paper sizes in tenths of mm + POINT *sizes = (POINT *)g_malloc0(count * sizeof(POINT)); + + DeviceCapabilitiesA(printer->name, NULL, DC_PAPERNAMES, names, NULL); + DeviceCapabilitiesA(printer->name, NULL, DC_PAPERSIZE, (LPSTR)sizes, NULL); + + for(DWORD k = 0; k < count; k++) + { + const char *paper_name = names + k * 64; + + // skip papers with zero dimension + if(sizes[k].x == 0 || sizes[k].y == 0) + continue; + + dt_paper_info_t *paper = malloc(sizeof(dt_paper_info_t)); + g_strlcpy(paper->name, paper_name, MAX_NAME); + g_strlcpy(paper->common_name, paper_name, MAX_NAME); + paper->width = (double)sizes[k].x / 10.0; // convert tenths of mm to mm + paper->height = (double)sizes[k].y / 10.0; + + result = g_list_append(result, paper); + + dt_print(DT_DEBUG_PRINT, + "[print] new paper %4lu %6.2f x %6.2f (%s)", + (unsigned long)k, paper->width, paper->height, paper->name); + } + + g_free(names); + g_free(sizes); + ClosePrinter(hPrinter); + + result = g_list_sort_with_data(result, (GCompareDataFunc)sort_papers, NULL); + return result; +} + +GList *dt_get_media_type(const dt_printer_info_t *printer) +{ + // Windows does not expose media type the same way CUPS/PPD does. + // Return an empty list — the media type combo will be hidden or + // show only "default". + return NULL; +} + +dt_medium_info_t *dt_get_medium(GList *media, + const char *name) +{ + dt_medium_info_t *result = NULL; + + for(GList *m = media; m; m = g_list_next(m)) + { + dt_medium_info_t *mi = (dt_medium_info_t *)m->data; + if(!strcmp(mi->name, name) || !strcmp(mi->common_name, name)) + { + result = mi; + break; + } + } + return result; +} + +void dt_print_file(const dt_imgid_t imgid, + const char *filename, + const char *job_title, + const dt_print_info_t *pinfo) +{ + // first for safety check that filename exists and is readable + + if(!g_file_test(filename, G_FILE_TEST_IS_REGULAR)) + { + dt_control_log(_("file `%s' to print not found for image %d on `%s'"), + filename, imgid, pinfo->printer.name); + return; + } + + // On Windows we print the PDF file by rendering it to a GDI DC. + // For the initial implementation we use ShellExecute with the + // "print" verb which delegates to the system PDF handler. + // This avoids reimplementing a full PDF rasteriser. + // + // A future enhancement could render the image bitmap directly + // using StretchDIBits to a printer DC. + + const HINSTANCE result = ShellExecuteA(NULL, "print", filename, + NULL, NULL, SW_HIDE); + + if((intptr_t)result <= 32) + { + dt_control_log(_("error while printing `%s' on `%s'"), job_title, pinfo->printer.name); + dt_print(DT_DEBUG_ALWAYS, + "[print] ShellExecute('print') failed for %s, code %d", + filename, (int)(intptr_t)result); + } + else + dt_control_log(_("printing `%s' on `%s'"), job_title, pinfo->printer.name); +} + +void dt_get_print_layout(const dt_print_info_t *prt, + const int32_t area_width, + const int32_t area_height, + float *px, + float *py, + float *pwidth, + float *pheight, + float *ax, + float *ay, + float *awidth, + float *aheight, + gboolean *borderless) +{ + /* this is where the layout is done for the display and for the + print too. So this routine is one of the most critical for the + print circuitry. */ + + // page w/h + float pg_width = prt->paper.width; + float pg_height = prt->paper.height; + + /* here, width and height correspond to the area for the picture */ + + // non-printable + float np_top = prt->printer.hw_margin_top; + float np_left = prt->printer.hw_margin_left; + float np_right = prt->printer.hw_margin_right; + float np_bottom = prt->printer.hw_margin_bottom; + + /* do some arrangements for the landscape mode. */ + + if(prt->page.landscape) + { + float tmp = pg_width; + pg_width = pg_height; + pg_height = tmp; + + // rotate the non-printable margins + tmp = np_top; + np_top = np_right; + np_right = np_bottom; + np_bottom = np_left; + np_left = tmp; + } + + // the image area aspect + const float a_aspect = (float)area_width / (float)area_height; + + // page aspect + const float pg_aspect = pg_width / pg_height; + + // display page + float p_bottom, p_right; + + if(a_aspect > pg_aspect) + { + *px = (area_width - (area_height * pg_aspect)) / 2.0f; + *py = 0; + p_bottom = area_height; + p_right = area_width - *px; + } + else + { + *px = 0; + *py = (area_height - (area_width / pg_aspect)) / 2.0f; + p_right = area_width; + p_bottom = area_height - *py; + } + + *pwidth = p_right - *px; + *pheight = p_bottom - *py; + + // page margins, note that we do not want to change those values for + // the landscape mode. these margins are those set by the user from + // the GUI, and the top margin is *always* at the top of the screen. + + const float border_top = prt->page.margin_top; + const float border_left = prt->page.margin_left; + const float border_right = prt->page.margin_right; + const float border_bottom = prt->page.margin_bottom; + + // display picture area, that is removing the non printable areas + // and user's margins + + const float bx = *px + (border_left / pg_width) * (*pwidth); + const float by = *py + (border_top / pg_height) * (*pheight); + const float bb = p_bottom - (border_bottom / pg_height) * (*pheight); + const float br = p_right - (border_right / pg_width) * (*pwidth); + + *borderless = border_left < np_left + || border_right < np_right + || border_top < np_top + || border_bottom < np_bottom; + + // now we have the printable area (ax, ay) -> (ax + awidth, ay + aheight) + + *ax = bx; + *ay = by; + *awidth = br - bx; + *aheight = bb - by; +} + +#endif /* _WIN32 */ + +// clang-format off +// modelines: These editor modelines have been set for all relevant files by tools/update_modelines.py +// vim: shiftwidth=2 expandtab tabstop=2 cindent +// kate: tab-indents: off; indent-width 2; replace-tabs on; indent-mode cstyle; remove-trailing-spaces modified; +// clang-format on diff --git a/src/libs/CMakeLists.txt b/src/libs/CMakeLists.txt index b9b57d1c9a2a..e7ff54848e46 100644 --- a/src/libs/CMakeLists.txt +++ b/src/libs/CMakeLists.txt @@ -108,10 +108,10 @@ add_library(geotagging MODULE "geotagging.c") set(MODULES ${MODULES} geotagging) # the module specific to print mode -if(CUPS_FOUND) +if(BUILD_PRINT) add_library(print_settings MODULE "print_settings.c") set(MODULES ${MODULES} print_settings) -endif(CUPS_FOUND) +endif(BUILD_PRINT) # AI neural restore module if(USE_AI) diff --git a/src/views/CMakeLists.txt b/src/views/CMakeLists.txt index e13a6f1af1d9..0e1df0137778 100644 --- a/src/views/CMakeLists.txt +++ b/src/views/CMakeLists.txt @@ -22,10 +22,10 @@ if(Gphoto2_FOUND) set(MODULES ${MODULES} tethering) endif(Gphoto2_FOUND) -if(CUPS_FOUND) +if(BUILD_PRINT) add_library(print MODULE "print.c") set(MODULES ${MODULES} print) -endif(CUPS_FOUND) +endif(BUILD_PRINT) foreach(module ${MODULES}) target_link_libraries(${module} lib_darktable) From 7758c1a7e2f513ffeeb8d656e4a118febf338edf Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 18 Apr 2026 00:45:30 +0530 Subject: [PATCH 2/5] fix(print): address Copilot review feedback on win_print.c - Add missing include for ShellExecuteW - Switch from ANSI (A) to wide (W) Win32 APIs for proper UTF-8 support via g_utf8_to_utf16/g_utf16_to_utf8 conversion - Use "WINSPOOL" as driver name in CreateDCW for reliable DC creation - Fix printer status filter to check PAUSED, OFFLINE, NOT_AVAILABLE in addition to ERROR - Check _cancel flag during printer enumeration loop for proper abort support - Use "printto" verb with printer name in ShellExecuteW to target the user-selected printer instead of system default - Fix sort_papers to use g_list_sort() instead of g_list_sort_with_data() to match its 2-argument signature - Add gdi32 and shell32 to Windows link libraries in CMakeLists --- src/CMakeLists.txt | 2 +- src/common/win_print.c | 130 ++++++++++++++++++++++++++++------------- 2 files changed, 91 insertions(+), 41 deletions(-) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 1254292353fc..ff3b40652976 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -923,7 +923,7 @@ if(BUILD_PRINT) "common/printing.c" ) set(SOURCES ${SOURCES} ${SOURCE_FILES_PRINT}) - list(APPEND LIBS winspool) + list(APPEND LIBS winspool gdi32 shell32) add_definitions("-DHAVE_PRINT") message(STATUS "Print mode: enabled (Windows native)") else(WIN32) diff --git a/src/common/win_print.c b/src/common/win_print.c index f5365d04f87e..664768d296c8 100644 --- a/src/common/win_print.c +++ b/src/common/win_print.c @@ -23,6 +23,7 @@ #ifdef _WIN32 #include +#include #include #include #include @@ -42,6 +43,22 @@ typedef struct dt_prtctl_t void *user_data; } dt_prtctl_t; +/* helper: convert a UTF-16 wide string to a UTF-8 gchar*. + The caller must g_free() the result. */ +static gchar *_wchar_to_utf8(const wchar_t *wstr) +{ + if(!wstr) return g_strdup(""); + return g_utf16_to_utf8((const gunichar2 *)wstr, -1, NULL, NULL, NULL); +} + +/* helper: convert a UTF-8 string to a newly-allocated wide string. + The caller must g_free() the result. */ +static wchar_t *_utf8_to_wchar(const char *utf8) +{ + if(!utf8) return NULL; + return (wchar_t *)g_utf8_to_utf16(utf8, -1, NULL, NULL, NULL); +} + // initialize the pinfo structure void dt_init_print_info(dt_print_info_t *pinfo) { @@ -63,8 +80,12 @@ void dt_get_printer_info(const char *printer_name, // default resolution pinfo->resolution = 300; - // try to get hardware margins from DEVMODE - HDC hdc = CreateDCA(NULL, printer_name, NULL, NULL); + // try to get hardware margins via a printer DC + // Use "WINSPOOL" as driver name for proper printer DC creation + wchar_t *wprinter = _utf8_to_wchar(printer_name); + HDC hdc = CreateDCW(L"WINSPOOL", wprinter, NULL, NULL); + g_free(wprinter); + if(hdc) { // get physical page size and printable area (in device units) @@ -94,6 +115,8 @@ void dt_get_printer_info(const char *printer_name, } } +static volatile int _cancel = 0; + static int _detect_printers_callback(dt_job_t *job) { dt_prtctl_t *pctl = dt_control_job_get_params(job); @@ -101,7 +124,7 @@ static int _detect_printers_callback(dt_job_t *job) DWORD needed = 0, returned = 0; // first call to find out how much memory we need - EnumPrintersA(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, + EnumPrintersW(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, NULL, 2, NULL, 0, &needed, &returned); if(needed == 0) @@ -117,27 +140,40 @@ static int _detect_printers_callback(dt_job_t *job) return 1; } - if(EnumPrintersA(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, + if(EnumPrintersW(PRINTER_ENUM_LOCAL | PRINTER_ENUM_CONNECTIONS, NULL, 2, buffer, needed, &needed, &returned)) { - PRINTER_INFO_2A *pi = (PRINTER_INFO_2A *)buffer; + PRINTER_INFO_2W *pi = (PRINTER_INFO_2W *)buffer; for(DWORD i = 0; i < returned; i++) { - // skip printers that are paused or have errors - if(pi[i].Status == 0 || !(pi[i].Status & PRINTER_STATUS_ERROR)) - { - dt_printer_info_t pr; - memset(&pr, 0, sizeof(pr)); - dt_get_printer_info(pi[i].pPrinterName, &pr); - if(pctl->cb) pctl->cb(&pr, pctl->user_data); - dt_print(DT_DEBUG_PRINT, "[print] new printer %s found", pi[i].pPrinterName); - } - else + // check for cancellation + if(_cancel) break; + + // skip printers that are paused, offline, or have errors + const DWORD bad_status = PRINTER_STATUS_ERROR + | PRINTER_STATUS_PAUSED + | PRINTER_STATUS_OFFLINE + | PRINTER_STATUS_NOT_AVAILABLE; + + if(pi[i].Status & bad_status) { + gchar *name_utf8 = _wchar_to_utf8(pi[i].pPrinterName); dt_print(DT_DEBUG_PRINT, "[print] skip printer %s (status=%lu)", - pi[i].pPrinterName, (unsigned long)pi[i].Status); + name_utf8, (unsigned long)pi[i].Status); + g_free(name_utf8); + continue; } + + gchar *name_utf8 = _wchar_to_utf8(pi[i].pPrinterName); + + dt_printer_info_t pr; + memset(&pr, 0, sizeof(pr)); + dt_get_printer_info(name_utf8, &pr); + if(pctl->cb) pctl->cb(&pr, pctl->user_data); + dt_print(DT_DEBUG_PRINT, "[print] new printer %s found", name_utf8); + + g_free(name_utf8); } } @@ -146,8 +182,6 @@ static int _detect_printers_callback(dt_job_t *job) return 0; } -static int _cancel = 0; - void dt_printers_abort_discovery(void) { _cancel = 1; @@ -156,6 +190,8 @@ void dt_printers_abort_discovery(void) void dt_printers_discovery(void (*cb)(dt_printer_info_t *pr, void *user_data), void *user_data) { + _cancel = 0; + // asynchronously checks for available printers dt_job_t *job = dt_control_job_create(&_detect_printers_callback, "detect connected printers"); if(job) @@ -200,38 +236,47 @@ sort_papers(gconstpointer p1, gconstpointer p2) GList *dt_get_papers(const dt_printer_info_t *printer) { GList *result = NULL; - HANDLE hPrinter = NULL; - if(!OpenPrinterA((LPSTR)printer->name, &hPrinter, NULL)) + wchar_t *wprinter = _utf8_to_wchar(printer->name); + if(!wprinter) return NULL; + + HANDLE hPrinter = NULL; + if(!OpenPrinterW(wprinter, &hPrinter, NULL)) + { + g_free(wprinter); return NULL; + } // get the number of paper names - const DWORD count = DeviceCapabilitiesA(printer->name, NULL, DC_PAPERNAMES, NULL, NULL); + const DWORD count = DeviceCapabilitiesW(wprinter, NULL, DC_PAPERNAMES, NULL, NULL); if(count == 0 || count == (DWORD)-1) { ClosePrinter(hPrinter); + g_free(wprinter); return NULL; } - // paper names are fixed 64-char blocks - char *names = (char *)g_malloc0(count * 64); + // paper names are fixed 64-wchar_t blocks + wchar_t *names = (wchar_t *)g_malloc0(count * 64 * sizeof(wchar_t)); // paper sizes in tenths of mm POINT *sizes = (POINT *)g_malloc0(count * sizeof(POINT)); - DeviceCapabilitiesA(printer->name, NULL, DC_PAPERNAMES, names, NULL); - DeviceCapabilitiesA(printer->name, NULL, DC_PAPERSIZE, (LPSTR)sizes, NULL); + DeviceCapabilitiesW(wprinter, NULL, DC_PAPERNAMES, (LPWSTR)names, NULL); + DeviceCapabilitiesW(wprinter, NULL, DC_PAPERSIZE, (LPWSTR)sizes, NULL); for(DWORD k = 0; k < count; k++) { - const char *paper_name = names + k * 64; + const wchar_t *wname = names + k * 64; // skip papers with zero dimension if(sizes[k].x == 0 || sizes[k].y == 0) continue; + gchar *paper_name_utf8 = _wchar_to_utf8(wname); + dt_paper_info_t *paper = malloc(sizeof(dt_paper_info_t)); - g_strlcpy(paper->name, paper_name, MAX_NAME); - g_strlcpy(paper->common_name, paper_name, MAX_NAME); + g_strlcpy(paper->name, paper_name_utf8, MAX_NAME); + g_strlcpy(paper->common_name, paper_name_utf8, MAX_NAME); paper->width = (double)sizes[k].x / 10.0; // convert tenths of mm to mm paper->height = (double)sizes[k].y / 10.0; @@ -240,13 +285,16 @@ GList *dt_get_papers(const dt_printer_info_t *printer) dt_print(DT_DEBUG_PRINT, "[print] new paper %4lu %6.2f x %6.2f (%s)", (unsigned long)k, paper->width, paper->height, paper->name); + + g_free(paper_name_utf8); } g_free(names); g_free(sizes); + g_free(wprinter); ClosePrinter(hPrinter); - result = g_list_sort_with_data(result, (GCompareDataFunc)sort_papers, NULL); + result = g_list_sort(result, sort_papers); return result; } @@ -289,26 +337,28 @@ void dt_print_file(const dt_imgid_t imgid, return; } - // On Windows we print the PDF file by rendering it to a GDI DC. - // For the initial implementation we use ShellExecute with the - // "print" verb which delegates to the system PDF handler. - // This avoids reimplementing a full PDF rasteriser. - // - // A future enhancement could render the image bitmap directly - // using StretchDIBits to a printer DC. + // Use the "printto" verb so we can target the specific printer + // selected in the darktable UI, not just the system default. + // Syntax: ShellExecute(NULL, "printto", file, "PrinterName", ...) + + wchar_t *wfilename = _utf8_to_wchar(filename); + wchar_t *wprinter = _utf8_to_wchar(pinfo->printer.name); - const HINSTANCE result = ShellExecuteA(NULL, "print", filename, - NULL, NULL, SW_HIDE); + const HINSTANCE result = ShellExecuteW(NULL, L"printto", wfilename, + wprinter, NULL, SW_HIDE); if((intptr_t)result <= 32) { dt_control_log(_("error while printing `%s' on `%s'"), job_title, pinfo->printer.name); dt_print(DT_DEBUG_ALWAYS, - "[print] ShellExecute('print') failed for %s, code %d", - filename, (int)(intptr_t)result); + "[print] ShellExecuteW('printto') failed for %s on %s, code %d", + filename, pinfo->printer.name, (int)(intptr_t)result); } else dt_control_log(_("printing `%s' on `%s'"), job_title, pinfo->printer.name); + + g_free(wfilename); + g_free(wprinter); } void dt_get_print_layout(const dt_print_info_t *prt, From 6b651b7ac4a9ed13020e4501e72183e9a6f1f054 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 18 Apr 2026 22:20:02 +0530 Subject: [PATCH 3/5] fix(print): resolve maintainer review feedback on PR #20828 - Zero-initialize dt_image_pos to fix -Wmaybe-uninitialized compiler warning - Update copyright year in win_print.c to 2026 --- src/common/printing.c | 2 +- src/common/win_print.c | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/common/printing.c b/src/common/printing.c index ea14b0c56e66..08398e88b4be 100644 --- a/src/common/printing.c +++ b/src/common/printing.c @@ -329,7 +329,7 @@ void dt_printing_setup_image(dt_images_box *imgs, box->print.width = box->pos.width * imgs->page_width; box->print.height = box->pos.height * imgs->page_height; - dt_image_pos pos; + dt_image_pos pos = { 0 }; _align_pos(&box->print, box->alignment, box->exp_width, box->exp_height, &pos); box->print.x = pos.x; diff --git a/src/common/win_print.c b/src/common/win_print.c index 664768d296c8..bc99e9aa9a33 100644 --- a/src/common/win_print.c +++ b/src/common/win_print.c @@ -1,6 +1,6 @@ /* This file is part of darktable, - Copyright (C) 2025 darktable developers. + Copyright (C) 2026 darktable developers. darktable is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by From d3841bbc73f5606773f71d1ed1d5d3c95de050c4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 19 Apr 2026 23:46:41 +0530 Subject: [PATCH 4/5] fix: ensure winsock2.h is included before windows.h in win_print.c The Windows API requires that winsock2.h must be included before windows.h to avoid conflicts. This fix reorders the includes in win_print.c so that: - winsock2.h is first - windows.h follows - Other headers follow in logical groups This resolves the MSYS2/Windows CI build failure with: 'winsock2.h:15: error: #warning Please include winsock2.h before windows.h' The entire win_print.c is guarded by #ifdef _WIN32, so this change only affects Windows builds and is safe for Linux/macOS where the file is not compiled. --- src/common/win_print.c | 1 + 1 file changed, 1 insertion(+) diff --git a/src/common/win_print.c b/src/common/win_print.c index bc99e9aa9a33..0149dd9982d5 100644 --- a/src/common/win_print.c +++ b/src/common/win_print.c @@ -22,6 +22,7 @@ #ifdef _WIN32 +#include #include #include #include From 1e7116da697935aea7a1e8a80efede0a0bc7afd1 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 20 Apr 2026 00:08:28 +0530 Subject: [PATCH 5/5] fix(print): add NULL checks for UTF-8/UTF-16 conversions in Windows backend Address potential NULL pointer dereferences when encoding conversion fails: - Check wprinter before CreateDCW in dt_get_printer_info() - Check name_utf8 before logging skipped printers - Check name_utf8 before using in dt_get_printer_info() - Check paper_name_utf8 before copying to struct - Check both wfilename and wprinter before ShellExecuteW() Also fix malloc -> g_malloc0 in paper enumeration for consistency with project's glib allocation pattern. These changes prevent crashes if g_utf16_to_utf8() or g_utf8_to_utf16() encounter encoding errors or OOM conditions. --- src/common/win_print.c | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/common/win_print.c b/src/common/win_print.c index 0149dd9982d5..4343dfe4aca0 100644 --- a/src/common/win_print.c +++ b/src/common/win_print.c @@ -84,6 +84,8 @@ void dt_get_printer_info(const char *printer_name, // try to get hardware margins via a printer DC // Use "WINSPOOL" as driver name for proper printer DC creation wchar_t *wprinter = _utf8_to_wchar(printer_name); + if(!wprinter) return; + HDC hdc = CreateDCW(L"WINSPOOL", wprinter, NULL, NULL); g_free(wprinter); @@ -160,13 +162,17 @@ static int _detect_printers_callback(dt_job_t *job) if(pi[i].Status & bad_status) { gchar *name_utf8 = _wchar_to_utf8(pi[i].pPrinterName); - dt_print(DT_DEBUG_PRINT, "[print] skip printer %s (status=%lu)", - name_utf8, (unsigned long)pi[i].Status); - g_free(name_utf8); + if(name_utf8) + { + dt_print(DT_DEBUG_PRINT, "[print] skip printer %s (status=%lu)", + name_utf8, (unsigned long)pi[i].Status); + g_free(name_utf8); + } continue; } gchar *name_utf8 = _wchar_to_utf8(pi[i].pPrinterName); + if(!name_utf8) continue; dt_printer_info_t pr; memset(&pr, 0, sizeof(pr)); @@ -274,8 +280,9 @@ GList *dt_get_papers(const dt_printer_info_t *printer) continue; gchar *paper_name_utf8 = _wchar_to_utf8(wname); + if(!paper_name_utf8) continue; - dt_paper_info_t *paper = malloc(sizeof(dt_paper_info_t)); + dt_paper_info_t *paper = g_malloc0(sizeof(dt_paper_info_t)); g_strlcpy(paper->name, paper_name_utf8, MAX_NAME); g_strlcpy(paper->common_name, paper_name_utf8, MAX_NAME); paper->width = (double)sizes[k].x / 10.0; // convert tenths of mm to mm @@ -345,6 +352,14 @@ void dt_print_file(const dt_imgid_t imgid, wchar_t *wfilename = _utf8_to_wchar(filename); wchar_t *wprinter = _utf8_to_wchar(pinfo->printer.name); + if(!wfilename || !wprinter) + { + dt_control_log(_("error while printing `%s' on `%s'"), job_title, pinfo->printer.name); + g_free(wfilename); + g_free(wprinter); + return; + } + const HINSTANCE result = ShellExecuteW(NULL, L"printto", wfilename, wprinter, NULL, SW_HIDE);