#include "screenshot_notifications.h" #include "ocr.h" #include #include #include #include #include #include #include #include #include #include #include #ifdef __MINGW32__ // This SDK aliases BYTE and boolean, producing duplicate template specializations. // Notifications use the boolean specialization; IReference is unused here. #define ____FIReference_1_BYTE_INTERFACE_DEFINED__ #endif #include #include #include namespace ks { namespace { using Microsoft::WRL::ComPtr; namespace Notifications=ABI::Windows::UI::Notifications; namespace Xml=ABI::Windows::Data::Xml::Dom; constexpr wchar_t AppId[]=L"HrynCo.KSwitcher.Screenshot"; std::string notificationError; void check(HRESULT result) { if(FAILED(result)) { char message[100]; snprintf(message,sizeof(message),"Windows screenshot notification failed (0x%08lx)",(unsigned long)result); throw std::runtime_error(message); } } struct String { HSTRING value{}; explicit String(const std::wstring &s) { check(WindowsCreateString(s.c_str(),(UINT32)s.size(),&value)); } ~String() { WindowsDeleteString(value); } }; std::filesystem::path cacheDirectory() { PWSTR folder{}; check(SHGetKnownFolderPath(FOLDERID_LocalAppData,0,nullptr,&folder)); auto path=std::filesystem::path(folder)/L"KSwitcher"/L"ScreenshotNotifications"; CoTaskMemFree(folder); std::filesystem::create_directories(path); return path; } std::filesystem::path previewDirectory() { HANDLE token{}; if(!OpenProcessToken(GetCurrentProcess(),TOKEN_QUERY,&token)) check(HRESULT_FROM_WIN32(GetLastError())); DWORD size{}; GetTokenInformation(token,TokenUser,nullptr,0,&size); std::vector data(size); BOOL read=GetTokenInformation(token,TokenUser,data.data(),size,&size); DWORD error=read?ERROR_SUCCESS:GetLastError(); CloseHandle(token); check(HRESULT_FROM_WIN32(error)); auto user=reinterpret_cast(data.data())->User.Sid; LPWSTR sidText{}; if(!ConvertSidToStringSidW(user,&sidText)) check(HRESULT_FROM_WIN32(GetLastError())); std::wstring sid(sidText); LocalFree(sidText); PWSTR folder{}; check(SHGetKnownFolderPath(FOLDERID_ProgramData,0,nullptr,&folder)); auto path=std::filesystem::path(folder)/(L"KSwitcher.ScreenshotPreviews-"+sid); CoTaskMemFree(folder); // The notification broker cannot load previews from some profile directories. // Keep this cache outside the profile, private to the current user and SYSTEM. auto sddl=L"O:"+sid+L"D:P(A;OICI;FA;;;SY)(A;OICI;FA;;;"+sid+L")"; PSECURITY_DESCRIPTOR security{}; if(!ConvertStringSecurityDescriptorToSecurityDescriptorW(sddl.c_str(),SDDL_REVISION_1,&security,nullptr)) check(HRESULT_FROM_WIN32(GetLastError())); SECURITY_ATTRIBUTES attributes{sizeof(attributes),security,FALSE}; BOOL created=CreateDirectoryW(path.c_str(),&attributes); error=created?ERROR_SUCCESS:GetLastError(); LocalFree(security); if(error!=ERROR_ALREADY_EXISTS) check(HRESULT_FROM_WIN32(error)); // Do not use a pre-created directory or junction controlled by another user. DWORD flags=GetFileAttributesW(path.c_str()); if(flags==INVALID_FILE_ATTRIBUTES || !(flags&FILE_ATTRIBUTE_DIRECTORY) || (flags&FILE_ATTRIBUTE_REPARSE_POINT)) throw std::runtime_error("Invalid screenshot preview directory"); PSID owner{}; PSECURITY_DESCRIPTOR existing{}; error=GetNamedSecurityInfoW(const_cast(path.c_str()),SE_FILE_OBJECT,OWNER_SECURITY_INFORMATION, &owner,nullptr,nullptr,nullptr,&existing); check(HRESULT_FROM_WIN32(error)); bool owned=EqualSid(owner,user); LocalFree(existing); if(!owned) throw std::runtime_error("Screenshot preview directory belongs to another user"); return path; } void cleanCache(const std::filesystem::path &path) { std::vector files; for(const auto &file:std::filesystem::directory_iterator(path)) if(file.is_regular_file() && validScreenshotToken(file.path().stem().wstring()) && (file.path().extension()==L".capture" || file.path().extension()==L".png")) files.push_back(file); std::sort(files.begin(),files.end(),[](const auto &a,const auto &b){return a.last_write_time()>b.last_write_time();}); uintmax_t size=0; for(size_t i=0;i=40 || size>256*1024*1024 || std::filesystem::file_time_type::clock::now()-files[i].last_write_time()>std::chrono::hours(24)) { std::error_code ignored; std::filesystem::remove(files[i].path(),ignored); } } } void savePreview(const Clip &clip,const std::filesystem::path &path) { auto pixels=decodeImage(clip,360,220); for(size_t i=3;i factory; check(CoCreateInstance(CLSID_WICImagingFactory,nullptr,CLSCTX_INPROC_SERVER,IID_PPV_ARGS(&factory))); ComPtr stream; check(factory->CreateStream(&stream)); check(stream->InitializeFromFilename(path.c_str(),GENERIC_WRITE)); ComPtr encoder; check(factory->CreateEncoder(GUID_ContainerFormatPng,nullptr,&encoder)); check(encoder->Initialize(stream.Get(),WICBitmapEncoderNoCache)); ComPtr frame; check(encoder->CreateNewFrame(&frame,nullptr)); check(frame->Initialize(nullptr)); check(frame->SetSize(pixels.width,pixels.height)); auto format=GUID_WICPixelFormat32bppBGRA; check(frame->SetPixelFormat(&format)); if(format!=GUID_WICPixelFormat32bppBGRA) throw std::runtime_error("Unsupported preview pixel format"); check(frame->WritePixels(pixels.height,pixels.width*4,(UINT)pixels.bgra.size(),pixels.bgra.data())); check(frame->Commit()); check(encoder->Commit()); } std::wstring escapeXml(const std::wstring &s) { std::wstring result; for(auto ch:s) { if(ch==L'&') result+=L"&"; else if(ch==L'\"') result+=L"""; else if(ch==L'<') result+=L"<"; else result+=ch; } return result; } } std::wstring cacheScreenshotForEditor(const Clip &clip) { // Normalize PNG/DIBV5 and other supported clipboard images at full resolution. auto pixels=decodeImage(clip); if(pixels.bgra.empty() || pixels.bgra.size()>128*1024*1024-sizeof(BITMAPINFOHEADER)) throw std::runtime_error("Image is too large for the screenshot editor"); auto dib=imageDib(pixels); auto directory=cacheDirectory(); cleanCache(directory); GUID guid{}; check(CoCreateGuid(&guid)); wchar_t value[40]{}; StringFromGUID2(guid,value,40); std::wstring id(value+1,36); atomicWrite(directory/(id+L".capture"),protect(dib.data)); return id; } Clip loadNotifiedScreenshot(const std::wstring &id) { if(!validScreenshotToken(id)) throw std::runtime_error("Invalid screenshot notification"); auto directory=cacheDirectory(); cleanCache(directory); auto path=directory/(id+L".capture"); if(!std::filesystem::exists(path)) throw std::runtime_error("This screenshot notification has expired. Take a new screenshot."); return Clip{{{CF_DIB,L"",unprotect(readFile(path,160*1024*1024))}}}; } bool notifyScreenshot(const Clip &clip) { notificationError.clear(); const char *stage="registration"; try { registerScreenshotNotifications(); check(SetCurrentProcessExplicitAppUserModelID(AppId)); stage="notification manager"; ComPtr manager; String managerClass(L"Windows.UI.Notifications.ToastNotificationManager"); check(RoGetActivationFactory(managerClass.value,IID_PPV_ARGS(&manager))); ComPtr notifier; String appId(AppId); stage="notifier"; check(manager->CreateToastNotifierWithId(appId.value,¬ifier)); stage="settings"; Notifications::NotificationSetting setting{}; auto settingResult=notifier->get_Setting(&setting); // A first-time sender has no settings record until its first notification. if(settingResult!=HRESULT_FROM_WIN32(ERROR_NOT_FOUND)) check(settingResult); if(setting!=Notifications::NotificationSetting_Enabled) throw std::runtime_error("Screenshot notifications are disabled in Windows settings"); stage="cache"; auto directory=cacheDirectory(); cleanCache(directory); GUID guid{}; check(CoCreateGuid(&guid)); wchar_t value[40]{}; StringFromGUID2(guid,value,40); std::wstring id(value+1,36); auto dib=std::find_if(clip.formats.begin(),clip.formats.end(),[](const auto &format){return format.id==CF_DIB;}); if(dib==clip.formats.end() || dib->data.size()>128*1024*1024) return false; atomicWrite(directory/(id+L".capture"),protect(dib->data)); stage="preview"; auto previews=previewDirectory(); cleanCache(previews); auto preview=previews/(id+L".png"); savePreview(clip,preview); cleanCache(previews); cleanCache(directory); stage="XML"; String documentClass(L"Windows.Data.Xml.Dom.XmlDocument"); ComPtr documentInstance; check(RoActivateInstance(documentClass.value,&documentInstance)); ComPtr document; check(documentInstance.As(&document)); ComPtr documentIO; check(document.As(&documentIO)); auto uri=L"file:///"+preview.generic_wstring(); String content(L"" L"Screenshot copiedClick to edit and annotate\"Screenshot"); check(documentIO->LoadXml(content.value)); stage="toast factory"; ComPtr toastFactory; String toastClass(L"Windows.UI.Notifications.ToastNotification"); check(RoGetActivationFactory(toastClass.value,IID_PPV_ARGS(&toastFactory))); ComPtr toast; check(toastFactory->CreateToastNotification(document.Get(),&toast)); ComPtr values; String propertyClass(L"Windows.Foundation.PropertyValue"); check(RoGetActivationFactory(propertyClass.value,IID_PPV_ARGS(&values))); FILETIME now{}; GetSystemTimeAsFileTime(&now); ULARGE_INTEGER timestamp{}; timestamp.LowPart=now.dwLowDateTime; timestamp.HighPart=now.dwHighDateTime; ABI::Windows::Foundation::DateTime expires{(INT64)(timestamp.QuadPart+24ull*60*60*10000000)}; ComPtr boxed; check(values->CreateDateTime(expires,&boxed)); ComPtr> expiration; check(boxed.As(&expiration)); check(toast->put_ExpirationTime(expiration.Get())); stage="show"; check(notifier->Show(toast.Get())); return true; } catch(const std::exception &e) { notificationError=std::string(stage)+": "+e.what(); log(wide(notificationError)); return false; } // Clipboard is already populated; fall back to the editor. } std::string screenshotNotificationError() { return notificationError; } std::wstring updateToastXml(const std::wstring &version) { return L"" L"KSwitcher update availableVersion " + escapeXml(version) + L" is available. Click to review and install." L"" L""; } bool notifyUpdateAvailable(const std::wstring &version) { try { // Reuse the screenshot notification identity, Windows settings and COM activator. registerScreenshotNotifications(); ComPtr manager; String managerClass(L"Windows.UI.Notifications.ToastNotificationManager"); check(RoGetActivationFactory(managerClass.value,IID_PPV_ARGS(&manager))); ComPtr notifier; String appId(AppId); check(manager->CreateToastNotifierWithId(appId.value,¬ifier)); Notifications::NotificationSetting setting{}; auto settingsResult=notifier->get_Setting(&setting); if(settingsResult!=HRESULT_FROM_WIN32(ERROR_NOT_FOUND)) check(settingsResult); if(setting!=Notifications::NotificationSetting_Enabled) throw std::runtime_error("Windows notifications for KSwitcher Screenshot are disabled"); String documentClass(L"Windows.Data.Xml.Dom.XmlDocument"); ComPtr instance; check(RoActivateInstance(documentClass.value,&instance)); ComPtr document; check(instance.As(&document)); ComPtr io; check(document.As(&io)); String content(updateToastXml(version)); check(io->LoadXml(content.value)); ComPtr factory; String toastClass(L"Windows.UI.Notifications.ToastNotification"); check(RoGetActivationFactory(toastClass.value,IID_PPV_ARGS(&factory))); ComPtr toast; check(factory->CreateToastNotification(document.Get(),&toast)); // Repeated submissions replace the existing update card in Notification Center. ComPtr tagged; check(toast.As(&tagged)); String tag(L"update"),group(L"KSwitcher"); check(tagged->put_Tag(tag.value)); check(tagged->put_Group(group.value)); check(notifier->Show(toast.Get())); return true; } catch(const std::exception &e) { log(L"Update toast: "+wide(e.what())); return false; } } }