From 4bb557ec41fa8b6383a66b1332c44c67810703a7 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 21 Oct 2020 21:58:06 +0530 Subject: [PATCH] Start wrapping of windows SAPI interface Can now get list of all voices installed in the system --- setup/extensions.json | 8 ++ src/calibre/constants.py | 2 +- src/calibre/utils/windows/common.h | 55 +++++++++ src/calibre/utils/windows/winsapi.cpp | 163 ++++++++++++++++++++++++++ 4 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 src/calibre/utils/windows/winsapi.cpp diff --git a/setup/extensions.json b/setup/extensions.json index 8140463c55..2c9d5187de 100644 --- a/setup/extensions.json +++ b/setup/extensions.json @@ -144,6 +144,14 @@ "libraries": "shell32 wininet advapi32", "cflags": "/X" }, + { + "name": "winsapi", + "only": "windows", + "headers": "calibre/utils/windows/common.h", + "sources": "calibre/utils/windows/winsapi.cpp", + "libraries": "SAPI Ole32", + "cflags": "/X" + }, { "name": "wpd", "only": "windows", diff --git a/src/calibre/constants.py b/src/calibre/constants.py index baeb456a4b..8d236b84b0 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -249,7 +249,7 @@ class ExtensionsImporter: 'certgen', ) if iswindows: - extra = ('winutil', 'wpd', 'winfonts') + extra = ('winutil', 'wpd', 'winfonts', 'winsapi') elif ismacos: extra = ('usbobserver', 'cocoa', 'libusb', 'libmtp') elif isfreebsd or ishaiku or islinux: diff --git a/src/calibre/utils/windows/common.h b/src/calibre/utils/windows/common.h index 6cb498b9f1..deaa70fbf5 100644 --- a/src/calibre/utils/windows/common.h +++ b/src/calibre/utils/windows/common.h @@ -7,8 +7,21 @@ #pragma once #define PY_SSIZE_T_CLEAN #define UNICODE +#define _UNICODE #include #include +#include + +static inline PyObject* +set_error_from_hresult(const char *file, const int line, const HRESULT hr, const char *prefix="") { + _com_error err(hr); + LPCWSTR msg = err.ErrorMessage(); + PyObject *pmsg = PyUnicode_FromWideChar(msg, -1); + PyObject *ans = PyErr_Format(PyExc_OSError, "%s:%d:%s:%V", file, line, prefix, pmsg, "Out of memory"); + Py_CLEAR(pmsg); + return ans; +} +#define error_from_hresult(hr, ...) set_error_from_hresult(__FILE__, __LINE__, hr, __VA_ARGS__) class wchar_raii { private: @@ -30,6 +43,48 @@ class wchar_raii { void set_ptr(wchar_t *val) { handle = val; } }; + +class com_wchar_raii { + private: + wchar_t *handle; + com_wchar_raii( const com_wchar_raii & ) ; + com_wchar_raii & operator=( const com_wchar_raii & ) ; + + public: + com_wchar_raii() : handle(NULL) {} + + ~com_wchar_raii() { + if (handle) { + CoTaskMemFree(handle); + handle = NULL; + } + } + + wchar_t *ptr() { return handle; } + wchar_t **address() { return &handle; } + explicit operator bool() const { return handle != NULL; } +}; + +class pyobject_raii { + private: + PyObject *handle; + pyobject_raii( const pyobject_raii & ) ; + pyobject_raii & operator=( const pyobject_raii & ) ; + + public: + pyobject_raii() : handle(NULL) {} + pyobject_raii(PyObject* h) : handle(h) {} + + ~pyobject_raii() { Py_CLEAR(handle); } + + PyObject *ptr() { return handle; } + void set_ptr(PyObject *val) { handle = val; } + PyObject **address() { return &handle; } + explicit operator bool() const { return handle != NULL; } + PyObject *detach() { PyObject *ans = handle; handle = NULL; return ans; } +}; + + class handle_raii { private: HANDLE handle; diff --git a/src/calibre/utils/windows/winsapi.cpp b/src/calibre/utils/windows/winsapi.cpp new file mode 100644 index 0000000000..3b3c918fb7 --- /dev/null +++ b/src/calibre/utils/windows/winsapi.cpp @@ -0,0 +1,163 @@ +/* + * winsapi.cpp + * Copyright (C) 2020 Kovid Goyal + * + * Distributed under terms of the GPL3 license. + */ + +#define _ATL_APARTMENT_THREADED +#include "common.h" + +#include +extern CComModule _Module; +#include + +#include +#pragma warning( push ) +#pragma warning( disable : 4996 ) // sphelper.h uses deprecated GetVersionEx +#include +#pragma warning( pop ) + +typedef struct { + PyObject_HEAD + ISpVoice *voice; +} Voice; + + +static PyTypeObject VoiceType = { + PyVarObject_HEAD_INIT(NULL, 0) +}; + +static PyObject * +Voice_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { + HRESULT hr = CoInitialize(NULL); + if (hr != S_OK && hr != S_FALSE) { + if (hr == RPC_E_CHANGED_MODE) { + return error_from_hresult(hr, "COM initialization failed as it was already initialized in multi-threaded mode"); + } + return PyErr_NoMemory(); + } + Voice *self = (Voice *) type->tp_alloc(type, 0); + if (self) { + if (FAILED(hr = CoCreateInstance(CLSID_SpVoice, NULL, CLSCTX_ALL, IID_ISpVoice, (void **)&self->voice))) { + Py_CLEAR(self); + return error_from_hresult(hr, "Failed to create ISpVoice instance"); + } + + } + return (PyObject*)self; +} + +static void +Voice_dealloc(Voice *self) { + if (self->voice) { self->voice->Release(); self->voice = NULL; } + CoUninitialize(); +} + + +static PyObject* +Voice_get_all_voices(Voice *self, PyObject *args) { + HRESULT hr = S_OK; + CComPtr iterator = NULL; + if (FAILED(hr = SpEnumTokens(SPCAT_VOICES, NULL, NULL, &iterator))) { + return error_from_hresult(hr, "Failed to create voice category iterator"); + return NULL; + } + pyobject_raii ans(PyList_New(0)); + if (!ans) return NULL; + while (true) { + CComPtr token = NULL; + if (FAILED(hr = iterator->Next(1, &token, NULL)) || hr == S_FALSE || !token) break; + pyobject_raii dict(PyDict_New()); + if (!dict) return NULL; + + com_wchar_raii id, description; + if (FAILED(hr = token->GetId(id.address()))) continue; + pyobject_raii idpy(PyUnicode_FromWideChar(id.ptr(), -1)); + if (!idpy) return NULL; + if (PyDict_SetItemString(dict.ptr(), "id", idpy.ptr()) != 0) return NULL; + + if (FAILED(hr = SpGetDescription(token, description.address(), NULL))) continue; + pyobject_raii descriptionpy(PyUnicode_FromWideChar(description.ptr(), -1)); + if (!descriptionpy) return NULL; + if (PyDict_SetItemString(dict.ptr(), "description", descriptionpy.ptr()) != 0) return NULL; + CComPtr attributes = NULL; + if (FAILED(hr = token->OpenKey(L"Attributes", &attributes))) continue; +#define ATTR(name) {\ + com_wchar_raii val; \ + if (SUCCEEDED(attributes->GetStringValue(TEXT(#name), val.address()))) { \ + pyobject_raii pyval(PyUnicode_FromWideChar(val.ptr(), -1)); if (!pyval) return NULL; \ + if (PyDict_SetItemString(dict.ptr(), #name, pyval.ptr()) != 0) return NULL; \ + }\ +} + ATTR(gender); ATTR(name); ATTR(vendor); ATTR(age); +#undef ATTR + com_wchar_raii val; + if (SUCCEEDED(attributes->GetStringValue(L"language", val.address()))) { + int lcid = wcstol(val.ptr(), NULL, 16); + wchar_t buf[LOCALE_NAME_MAX_LENGTH]; + if (LCIDToLocaleName(lcid, buf, LOCALE_NAME_MAX_LENGTH, 0) > 0) { + pyobject_raii pyval(PyUnicode_FromWideChar(buf, -1)); if (!pyval) return NULL; + if (PyDict_SetItemString(dict.ptr(), "language", pyval.ptr()) != 0) return NULL; + } + } + if (PyList_Append(ans.ptr(), dict.ptr()) != 0) return NULL; + } + return ans.detach(); +} + + +#define M(name, args) { #name, (PyCFunction)Voice_##name, args, ""} +static PyMethodDef Voice_methods[] = { + M(get_all_voices, METH_NOARGS), + {NULL, NULL, 0, NULL} +}; +#undef M + +#define M(name, args) { #name, name, args, ""} +static PyMethodDef winsapi_methods[] = { + {NULL, NULL, 0, NULL} +}; +#undef M + +static struct PyModuleDef winsapi_module = { + /* m_base */ PyModuleDef_HEAD_INIT, + /* m_name */ "winsapi", + /* m_doc */ "SAPI wrapper", + /* m_size */ -1, + /* m_methods */ winsapi_methods, + /* m_slots */ 0, + /* m_traverse */ 0, + /* m_clear */ 0, + /* m_free */ 0, +}; + + + +extern "C" { + +CALIBRE_MODINIT_FUNC PyInit_winsapi(void) { + VoiceType.tp_name = "winsapi.Voice"; + VoiceType.tp_doc = "Wrapper for ISpVoice"; + VoiceType.tp_basicsize = sizeof(Voice); + VoiceType.tp_itemsize = 0; + VoiceType.tp_flags = Py_TPFLAGS_DEFAULT; + VoiceType.tp_new = Voice_new; + VoiceType.tp_methods = Voice_methods; + VoiceType.tp_dealloc = (destructor)Voice_dealloc; + if (PyType_Ready(&VoiceType) < 0) return NULL; + + PyObject *m = PyModule_Create(&winsapi_module); + if (m == NULL) return NULL; + + Py_INCREF(&VoiceType); + if (PyModule_AddObject(m, "Voice", (PyObject *) &VoiceType) < 0) { + Py_DECREF(&VoiceType); + Py_DECREF(m); + return NULL; + } + + return m; +} + +}