diff --git a/README.md b/README.md index 8a26e5a..4510b35 100644 --- a/README.md +++ b/README.md @@ -7,13 +7,14 @@ NightShot — нативное приложение для Windows 10/11, пре ## Возможности - Захват произвольной области экрана через `PrintScreen` -- Поддержка нескольких мониторов и разных значений DPI -- Перемещение и изменение размера выделенной области +- Поддержка нескольких мониторов и разных значений DPI без полноразмерного DIB на весь bounding box +- Перемещение и изменение размера выделенной области только в режиме Drag +- Аннотации живут на всём виртуальном рабочем столе независимо от области кадрирования - Карандаш, линии, стрелки, прямоугольники, маркер и текст -- Настройка толщины инструментов колёсиком мыши +- Настройка толщины инструментов колёсиком мыши с постоянным hollow-preview размера кисти - ColorPicker с пипеткой, HEX, RGBA и HSV - Сохраняемая палитра цветов 8×6 -- Копирование изображения в буфер обмена +- Копирование изображения в буфер обмена с явным запросом сохранения в истории Win+V - Сохранение снимков без перезаписи существующих файлов - Запись GIF через `Ctrl + PrintScreen` - Выбор FPS, качества, курсора и лимита записи diff --git a/include/capture.hpp b/include/capture.hpp index 9979025..9c162e2 100644 --- a/include/capture.hpp +++ b/include/capture.hpp @@ -2,26 +2,57 @@ #include "config.hpp" #include -struct ScreenCapture { +#include +#include + +struct ScreenCaptureSurface { HDC memoryDc = nullptr; HBITMAP bitmap = nullptr; HGDIOBJ oldBitmap = nullptr; - void* pixels = nullptr; - int x = 0; - int y = 0; + std::uint8_t* pixels = nullptr; + RECT rect{}; // local coordinates inside the virtual-desktop window int width = 0; int height = 0; int stride = 0; - ScreenCapture() = default; - ScreenCapture(const ScreenCapture&) = delete; - ScreenCapture& operator=(const ScreenCapture&) = delete; - ScreenCapture(ScreenCapture&& other) noexcept; - ScreenCapture& operator=(ScreenCapture&& other) noexcept; - ~ScreenCapture(); + ScreenCaptureSurface() = default; + ScreenCaptureSurface(const ScreenCaptureSurface&) = delete; + ScreenCaptureSurface& operator=(const ScreenCaptureSurface&) = delete; + ScreenCaptureSurface(ScreenCaptureSurface&& other) noexcept; + ScreenCaptureSurface& operator=(ScreenCaptureSurface&& other) noexcept; + ~ScreenCaptureSurface(); - [[nodiscard]] bool valid() const noexcept { return memoryDc && bitmap && width > 0 && height > 0; } + [[nodiscard]] bool valid() const noexcept { + return memoryDc && bitmap && pixels && width > 0 && height > 0; + } void reset() noexcept; }; +struct ScreenCapture { + int x = 0; + int y = 0; + int width = 0; + int height = 0; + std::vector surfaces; + + ScreenCapture() = default; + ScreenCapture(const ScreenCapture&) = delete; + ScreenCapture& operator=(const ScreenCapture&) = delete; + ScreenCapture(ScreenCapture&&) noexcept = default; + ScreenCapture& operator=(ScreenCapture&&) noexcept = default; + ~ScreenCapture() = default; + + [[nodiscard]] bool valid() const noexcept { + return width > 0 && height > 0 && !surfaces.empty(); + } + + // Copies a local virtual-desktop rectangle into destination. Empty space + // between physically arranged monitors is filled with black instead of + // forcing NightShot to keep one enormous bounding-box DIB in RAM. + [[nodiscard]] bool blitTo(HDC destination, int destinationX, int destinationY, + const RECT& sourceLocal) const noexcept; + + [[nodiscard]] bool samplePixel(int localX, int localY, COLORREF& color) const noexcept; +}; + [[nodiscard]] ScreenCapture captureVirtualDesktop(); diff --git a/include/settings.hpp b/include/settings.hpp index 5162404..2790d0f 100644 --- a/include/settings.hpp +++ b/include/settings.hpp @@ -48,6 +48,9 @@ struct AppSettings { int rectangleWidth = 3; int markerWidth = 15; + // Last drawing color persists independently from the saved 8x6 palette. + std::uint32_t selectedColorArgb = 0xFF3B82F6u; + // ARGB values. Empty slots remain available for one-click saving from the // in-editor 8x6 color picker. std::array paletteArgb{}; diff --git a/res/app.rc b/res/app.rc index d873bf9..1db7da0 100644 --- a/res/app.rc +++ b/res/app.rc @@ -3,8 +3,8 @@ IDI_APP_ICON ICON "nightshot.ico" IDR_APP_MANIFEST RT_MANIFEST "app.manifest" 1 VERSIONINFO - FILEVERSION 0,17,0,0 - PRODUCTVERSION 0,17,0,0 + FILEVERSION 0,19,0,0 + PRODUCTVERSION 0,19,0,0 FILEFLAGSMASK 0x3fL FILEFLAGS 0x0L FILEOS 0x40004L @@ -17,11 +17,11 @@ BEGIN BEGIN VALUE "CompanyName", "NightSync" VALUE "FileDescription", "NightShot Win64 screenshot utility" - VALUE "FileVersion", "0.17.0" + VALUE "FileVersion", "0.19.0" VALUE "InternalName", "NightShot" VALUE "OriginalFilename", "NightShot.exe" VALUE "ProductName", "NightShot" - VALUE "ProductVersion", "0.17.0" + VALUE "ProductVersion", "0.19.0" END END BLOCK "VarFileInfo" diff --git a/src/capture.cpp b/src/capture.cpp index e5dd483..c730ce9 100644 --- a/src/capture.cpp +++ b/src/capture.cpp @@ -1,17 +1,96 @@ #include "capture.hpp" + +#include +#include #include -ScreenCapture::ScreenCapture(ScreenCapture&& other) noexcept { *this = std::move(other); } +namespace { -ScreenCapture& ScreenCapture::operator=(ScreenCapture&& other) noexcept { +RECT intersectRects(RECT first, RECT second) { + RECT result{ + std::max(first.left, second.left), + std::max(first.top, second.top), + std::min(first.right, second.right), + std::min(first.bottom, second.bottom)}; + if (result.right <= result.left || result.bottom <= result.top) return RECT{}; + return result; +} + +struct CaptureContext { + ScreenCapture* capture = nullptr; + HDC screenDc = nullptr; +}; + +BOOL CALLBACK captureMonitor(HMONITOR monitor, HDC, LPRECT, LPARAM parameter) { + auto* context = reinterpret_cast(parameter); + if (!context || !context->capture || !context->screenDc) return FALSE; + + MONITORINFO info{}; + info.cbSize = sizeof(info); + if (!GetMonitorInfoW(monitor, &info)) return TRUE; + + const int width = info.rcMonitor.right - info.rcMonitor.left; + const int height = info.rcMonitor.bottom - info.rcMonitor.top; + if (width <= 0 || height <= 0) return TRUE; + + ScreenCaptureSurface surface; + surface.width = width; + surface.height = height; + surface.stride = width * 4; + surface.rect = RECT{ + info.rcMonitor.left - context->capture->x, + info.rcMonitor.top - context->capture->y, + info.rcMonitor.right - context->capture->x, + info.rcMonitor.bottom - context->capture->y}; + + surface.memoryDc = CreateCompatibleDC(context->screenDc); + if (!surface.memoryDc) return TRUE; + + BITMAPINFO bitmapInfo{}; + bitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + bitmapInfo.bmiHeader.biWidth = width; + bitmapInfo.bmiHeader.biHeight = -height; + bitmapInfo.bmiHeader.biPlanes = 1; + bitmapInfo.bmiHeader.biBitCount = 32; + bitmapInfo.bmiHeader.biCompression = BI_RGB; + + void* rawPixels = nullptr; + surface.bitmap = CreateDIBSection( + context->screenDc, &bitmapInfo, DIB_RGB_COLORS, + &rawPixels, nullptr, 0); + surface.pixels = static_cast(rawPixels); + if (!surface.bitmap || !surface.pixels) return TRUE; + + surface.oldBitmap = SelectObject(surface.memoryDc, surface.bitmap); + if (!surface.oldBitmap || surface.oldBitmap == HGDI_ERROR) { + surface.oldBitmap = nullptr; + return TRUE; + } + + if (!BitBlt(surface.memoryDc, 0, 0, width, height, + context->screenDc, info.rcMonitor.left, info.rcMonitor.top, + SRCCOPY | CAPTUREBLT)) { + return TRUE; + } + + context->capture->surfaces.push_back(std::move(surface)); + return TRUE; +} + +} // namespace + +ScreenCaptureSurface::ScreenCaptureSurface(ScreenCaptureSurface&& other) noexcept { + *this = std::move(other); +} + +ScreenCaptureSurface& ScreenCaptureSurface::operator=(ScreenCaptureSurface&& other) noexcept { if (this != &other) { reset(); memoryDc = std::exchange(other.memoryDc, nullptr); bitmap = std::exchange(other.bitmap, nullptr); oldBitmap = std::exchange(other.oldBitmap, nullptr); pixels = std::exchange(other.pixels, nullptr); - x = std::exchange(other.x, 0); - y = std::exchange(other.y, 0); + rect = std::exchange(other.rect, RECT{}); width = std::exchange(other.width, 0); height = std::exchange(other.height, 0); stride = std::exchange(other.stride, 0); @@ -19,19 +98,68 @@ ScreenCapture& ScreenCapture::operator=(ScreenCapture&& other) noexcept { return *this; } -ScreenCapture::~ScreenCapture() { reset(); } +ScreenCaptureSurface::~ScreenCaptureSurface() { + reset(); +} -void ScreenCapture::reset() noexcept { - if (memoryDc && oldBitmap) { +void ScreenCaptureSurface::reset() noexcept { + if (memoryDc && oldBitmap && oldBitmap != HGDI_ERROR) { SelectObject(memoryDc, oldBitmap); } + oldBitmap = nullptr; if (bitmap) DeleteObject(bitmap); + bitmap = nullptr; if (memoryDc) DeleteDC(memoryDc); memoryDc = nullptr; - bitmap = nullptr; - oldBitmap = nullptr; pixels = nullptr; - x = y = width = height = stride = 0; + rect = RECT{}; + width = 0; + height = 0; + stride = 0; +} + +bool ScreenCapture::blitTo(HDC destination, int destinationX, int destinationY, + const RECT& sourceLocal) const noexcept { + if (!destination || sourceLocal.right <= sourceLocal.left || + sourceLocal.bottom <= sourceLocal.top) return false; + + const int widthToCopy = sourceLocal.right - sourceLocal.left; + const int heightToCopy = sourceLocal.bottom - sourceLocal.top; + PatBlt(destination, destinationX, destinationY, widthToCopy, heightToCopy, BLACKNESS); + + bool copied = false; + for (const ScreenCaptureSurface& surface : surfaces) { + if (!surface.valid()) continue; + const RECT overlap = intersectRects(sourceLocal, surface.rect); + if (overlap.right <= overlap.left || overlap.bottom <= overlap.top) continue; + + const int width = overlap.right - overlap.left; + const int height = overlap.bottom - overlap.top; + const int sourceX = overlap.left - surface.rect.left; + const int sourceY = overlap.top - surface.rect.top; + const int targetX = destinationX + overlap.left - sourceLocal.left; + const int targetY = destinationY + overlap.top - sourceLocal.top; + if (BitBlt(destination, targetX, targetY, width, height, + surface.memoryDc, sourceX, sourceY, SRCCOPY)) { + copied = true; + } + } + return copied; +} + +bool ScreenCapture::samplePixel(int localX, int localY, COLORREF& color) const noexcept { + for (const ScreenCaptureSurface& surface : surfaces) { + if (!surface.valid() || localX < surface.rect.left || localX >= surface.rect.right || + localY < surface.rect.top || localY >= surface.rect.bottom) continue; + const int xInSurface = localX - surface.rect.left; + const int yInSurface = localY - surface.rect.top; + const std::uint8_t* pixel = surface.pixels + + static_cast(yInSurface) * static_cast(surface.stride) + + static_cast(xInSurface) * 4u; + color = RGB(pixel[2], pixel[1], pixel[0]); + return true; + } + return false; } ScreenCapture captureVirtualDesktop() { @@ -40,30 +168,45 @@ ScreenCapture captureVirtualDesktop() { result.y = GetSystemMetrics(SM_YVIRTUALSCREEN); result.width = GetSystemMetrics(SM_CXVIRTUALSCREEN); result.height = GetSystemMetrics(SM_CYVIRTUALSCREEN); - result.stride = result.width * 4; if (result.width <= 0 || result.height <= 0) return result; HDC screenDc = GetDC(nullptr); if (!screenDc) return result; - result.memoryDc = CreateCompatibleDC(screenDc); - BITMAPINFO info{}; - info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); - info.bmiHeader.biWidth = result.width; - info.bmiHeader.biHeight = -result.height; - info.bmiHeader.biPlanes = 1; - info.bmiHeader.biBitCount = 32; - info.bmiHeader.biCompression = BI_RGB; + CaptureContext context{&result, screenDc}; + EnumDisplayMonitors(nullptr, nullptr, captureMonitor, + reinterpret_cast(&context)); - result.bitmap = CreateDIBSection(screenDc, &info, DIB_RGB_COLORS, &result.pixels, nullptr, 0); - if (result.memoryDc && result.bitmap) { - result.oldBitmap = SelectObject(result.memoryDc, result.bitmap); - if (!BitBlt(result.memoryDc, 0, 0, result.width, result.height, screenDc, - result.x, result.y, SRCCOPY | CAPTUREBLT)) { - result.reset(); + // Extremely defensive fallback for unusual display drivers which don't + // enumerate monitors through EnumDisplayMonitors. + if (result.surfaces.empty()) { + ScreenCaptureSurface surface; + surface.width = result.width; + surface.height = result.height; + surface.stride = result.width * 4; + surface.rect = RECT{0, 0, result.width, result.height}; + surface.memoryDc = CreateCompatibleDC(screenDc); + + BITMAPINFO info{}; + info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + info.bmiHeader.biWidth = result.width; + info.bmiHeader.biHeight = -result.height; + info.bmiHeader.biPlanes = 1; + info.bmiHeader.biBitCount = 32; + info.bmiHeader.biCompression = BI_RGB; + void* pixels = nullptr; + surface.bitmap = surface.memoryDc + ? CreateDIBSection(screenDc, &info, DIB_RGB_COLORS, &pixels, nullptr, 0) + : nullptr; + surface.pixels = static_cast(pixels); + if (surface.memoryDc && surface.bitmap && surface.pixels) { + surface.oldBitmap = SelectObject(surface.memoryDc, surface.bitmap); + if (surface.oldBitmap && surface.oldBitmap != HGDI_ERROR && + BitBlt(surface.memoryDc, 0, 0, result.width, result.height, + screenDc, result.x, result.y, SRCCOPY | CAPTUREBLT)) { + result.surfaces.push_back(std::move(surface)); + } } - } else { - result.reset(); } ReleaseDC(nullptr, screenDc); diff --git a/src/gif_recorder.cpp b/src/gif_recorder.cpp index e8cc7c0..dfc3d94 100644 --- a/src/gif_recorder.cpp +++ b/src/gif_recorder.cpp @@ -15,6 +15,7 @@ #include #include #include +#include #include #include #include @@ -37,7 +38,7 @@ using Clock = std::chrono::steady_clock; constexpr wchar_t kHudClass[] = L"NightShotGifRecordingHud"; constexpr COLORREF kTransparentKey = RGB(1, 2, 3); constexpr UINT_PTR kHudTimer = 1; -constexpr std::size_t kMaximumQueuedFrames = 4; +constexpr std::size_t kMaximumQueuedFrames = 2; #ifndef WDA_EXCLUDEFROMCAPTURE #define WDA_EXCLUDEFROMCAPTURE 0x00000011 #endif @@ -132,6 +133,9 @@ HGLOBAL globalBytes(const void* data, std::size_t bytes) { return memory; } +HGLOBAL serializedDword(DWORD value) { + return globalBytes(&value, sizeof(value)); +} HGLOBAL dropFiles(const std::wstring& path) { const std::size_t textBytes = (path.size() + 2) * sizeof(wchar_t); @@ -223,6 +227,29 @@ HGLOBAL firstFrameDib24(const Session& session) { return memory; } +HBITMAP firstFrameBitmap(const Session& session) { + if (session.firstFrame.empty() || session.width <= 0 || session.height <= 0) return nullptr; + BITMAPINFO info{}; + info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER); + info.bmiHeader.biWidth = session.width; + info.bmiHeader.biHeight = -session.height; + info.bmiHeader.biPlanes = 1; + info.bmiHeader.biBitCount = 32; + info.bmiHeader.biCompression = BI_RGB; + + void* pixels = nullptr; + HDC screen = GetDC(nullptr); + HBITMAP bitmap = CreateDIBSection(screen, &info, DIB_RGB_COLORS, &pixels, nullptr, 0); + if (screen) ReleaseDC(nullptr, screen); + if (!bitmap || !pixels) { + if (bitmap) DeleteObject(bitmap); + return nullptr; + } + std::memcpy(pixels, session.firstFrame.data(), + static_cast(session.stride) * session.height); + return bitmap; +} + bool openClipboardWithRetry(HWND owner) { for (int attempt = 0; attempt < 12; ++attempt) { if (OpenClipboard(owner)) return true; @@ -240,23 +267,30 @@ bool copyGifToClipboard(const Session& session) { HGLOBAL files = dropFiles(session.path); HGLOBAL dibV5 = firstFrameDibV5(session); HGLOBAL dib = firstFrameDib24(session); - if (!gifGeneric && !gifMime && !files && !dibV5 && !dib) return false; + HBITMAP bitmap = firstFrameBitmap(session); + HGLOBAL includeInHistory = serializedDword(1); + if (!gifGeneric && !gifMime && !files && !dibV5 && !dib && !bitmap) { + if (includeInHistory) GlobalFree(includeInHistory); + return false; + } - if (!openClipboardWithRetry(session.request.notificationTarget)) { + auto cleanup = [&]() { if (gifGeneric) GlobalFree(gifGeneric); if (gifMime) GlobalFree(gifMime); if (files) GlobalFree(files); if (dibV5) GlobalFree(dibV5); if (dib) GlobalFree(dib); + if (bitmap) DeleteObject(bitmap); + if (includeInHistory) GlobalFree(includeInHistory); + }; + + if (!openClipboardWithRetry(session.request.notificationTarget)) { + cleanup(); return false; } if (!EmptyClipboard()) { CloseClipboard(); - if (gifGeneric) GlobalFree(gifGeneric); - if (gifMime) GlobalFree(gifMime); - if (files) GlobalFree(files); - if (dibV5) GlobalFree(dibV5); - if (dib) GlobalFree(dib); + cleanup(); return false; } @@ -283,23 +317,96 @@ bool copyGifToClipboard(const Session& session) { dib = nullptr; transferred = true; } - CloseClipboard(); + if (bitmap && SetClipboardData(CF_BITMAP, bitmap)) { + bitmap = nullptr; + transferred = true; + } - if (gifGeneric) GlobalFree(gifGeneric); - if (gifMime) GlobalFree(gifMime); - if (files) GlobalFree(files); - if (dibV5) GlobalFree(dibV5); - if (dib) GlobalFree(dib); + // Explicitly ask Windows 10/11 clipboard monitoring to retain this item in + // Win+V. The first frame is also published as standard bitmap formats so + // history has a format it can display even when it ignores the custom GIF. + const UINT historyFormat = RegisterClipboardFormatW(L"CanIncludeInClipboardHistory"); + if (includeInHistory && historyFormat && SetClipboardData(historyFormat, includeInHistory)) { + includeInHistory = nullptr; + } + CloseClipboard(); + cleanup(); return transferred; } +bool ensureInfiniteLoopExtension(const std::wstring& path) { + std::vector bytes = readAllBytes(path); + static constexpr BYTE loopBlock[]{ + 0x21, 0xFF, 0x0B, + 'N','E','T','S','C','A','P','E','2','.','0', + 0x03, 0x01, 0x00, 0x00, 0x00}; + if (bytes.size() < 13 || + !(std::memcmp(bytes.data(), "GIF87a", 6) == 0 || + std::memcmp(bytes.data(), "GIF89a", 6) == 0)) return false; + + const auto found = std::search(bytes.begin(), bytes.end(), + std::begin(loopBlock), std::end(loopBlock)); + if (found != bytes.end()) return true; + + const BYTE packed = bytes[10]; + std::size_t insertion = 13; + if ((packed & 0x80u) != 0) { + const std::size_t colorCount = std::size_t{1} << ((packed & 0x07u) + 1u); + const std::size_t tableBytes = colorCount * 3u; + if (insertion + tableBytes > bytes.size()) return false; + insertion += tableBytes; + } + + bytes.insert(bytes.begin() + static_cast(insertion), + std::begin(loopBlock), std::end(loopBlock)); + const std::filesystem::path target(path); + const std::filesystem::path temporary = target.wstring() + L".loopfix"; + { + std::ofstream output(temporary, std::ios::binary | std::ios::trunc); + if (!output) return false; + output.write(reinterpret_cast(bytes.data()), + static_cast(bytes.size())); + if (!output) return false; + } + if (!MoveFileExW(temporary.c_str(), target.c_str(), + MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH)) { + DeleteFileW(temporary.c_str()); + return false; + } + return true; +} + +HRESULT setGlobalLoopMetadata(IWICBitmapEncoder* encoder) { + if (!encoder) return E_POINTER; + IWICMetadataQueryWriter* writer = nullptr; + HRESULT result = encoder->GetMetadataQueryWriter(&writer); + if (FAILED(result) || !writer) return FAILED(result) ? result : E_NOINTERFACE; + + BYTE application[]{'N','E','T','S','C','A','P','E','2','.','0'}; + PROPVARIANT value{}; + value.vt = VT_VECTOR | VT_UI1; + value.caub.cElems = static_cast(std::size(application)); + value.caub.pElems = application; + result = writer->SetMetadataByName(L"/appext/Application", &value); + + if (SUCCEEDED(result)) { + BYTE data[]{0x03, 0x01, 0x00, 0x00, 0x00}; + value = PROPVARIANT{}; + value.vt = VT_VECTOR | VT_UI1; + value.caub.cElems = static_cast(std::size(data)); + value.caub.pElems = data; + result = writer->SetMetadataByName(L"/appext/Data", &value); + } + releaseCom(writer); + return result; +} + int paletteColorCount(int quality) { static constexpr int colors[10]{32, 32, 64, 64, 128, 128, 256, 256, 256, 256}; return colors[std::clamp(quality, 1, 10) - 1]; } -HRESULT setFrameMetadata(IWICBitmapFrameEncode* frame, UINT16 delay, - bool firstFrame) { +HRESULT setFrameMetadata(IWICBitmapFrameEncode* frame, UINT16 delay) { IWICMetadataQueryWriter* writer = nullptr; HRESULT result = frame->GetMetadataQueryWriter(&writer); if (FAILED(result) || !writer) return FAILED(result) ? result : E_NOINTERFACE; @@ -313,9 +420,6 @@ HRESULT setFrameMetadata(IWICBitmapFrameEncode* frame, UINT16 delay, return result; } - // These fields improve interoperability but are not required to encode a - // valid frame. Some third-party WIC codecs reject optional metadata, so - // only the frame delay is treated as mandatory. value = PROPVARIANT{}; value.vt = VT_UI1; value.bVal = 1; @@ -326,21 +430,6 @@ HRESULT setFrameMetadata(IWICBitmapFrameEncode* frame, UINT16 delay, value.boolVal = VARIANT_FALSE; writer->SetMetadataByName(L"/grctlext/TransparencyFlag", &value); - if (firstFrame) { - static char application[] = "NETSCAPE2.0"; - value = PROPVARIANT{}; - value.vt = VT_LPSTR; - value.pszVal = application; - writer->SetMetadataByName(L"/appext/Application", &value); - - BYTE loopData[4]{3, 1, 0, 0}; - value = PROPVARIANT{}; - value.vt = VT_VECTOR | VT_UI1; - value.caub.cElems = static_cast(std::size(loopData)); - value.caub.pElems = loopData; - writer->SetMetadataByName(L"/appext/Data", &value); - } - releaseCom(writer); return S_OK; } @@ -358,6 +447,7 @@ public: width_ = width; height_ = height; quality_ = std::clamp(quality, 1, 10); + path_ = path; HRESULT result = CoCreateInstance( CLSID_WICImagingFactory, nullptr, CLSCTX_INPROC_SERVER, @@ -386,6 +476,7 @@ public: error = L"Не удалось инициализировать GIF encoder: " + hresultText(result); return false; } + (void)setGlobalLoopMetadata(encoder_); return true; } @@ -473,7 +564,7 @@ public: } if (SUCCEEDED(result)) { stage = L"GIF metadata"; - result = setFrameMetadata(encodedFrame, delay, frameCount_ == 0); + result = setFrameMetadata(encodedFrame, delay); } if (SUCCEEDED(result)) { // Let WIC convert BGRA directly into the indexed encoder format. @@ -518,6 +609,10 @@ public: releaseCom(encoder_); releaseCom(stream_); releaseCom(factory_); + if (!ensureInfiniteLoopExtension(path_)) { + error = L"GIF сохранён, но не удалось гарантировать бесконечное зацикливание."; + return false; + } return true; } @@ -529,6 +624,7 @@ private: int height_ = 0; int quality_ = 8; int frameCount_ = 0; + std::wstring path_; }; void drawCursorIntoCapture(HDC destination, const RECT& sourceRect) { diff --git a/src/image_io.cpp b/src/image_io.cpp index aca7b26..19c3caf 100644 --- a/src/image_io.cpp +++ b/src/image_io.cpp @@ -222,6 +222,19 @@ bool openClipboardWithRetry(HWND owner) { } return false; } + +HGLOBAL serializedDword(DWORD value) { + HGLOBAL memory = GlobalAlloc(GMEM_MOVEABLE, sizeof(DWORD)); + if (!memory) return nullptr; + void* destination = GlobalLock(memory); + if (!destination) { + GlobalFree(memory); + return nullptr; + } + std::memcpy(destination, &value, sizeof(value)); + GlobalUnlock(memory); + return memory; +} } // namespace bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) { @@ -232,13 +245,18 @@ bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) { HGLOBAL dibV5 = makeDibV5(image); HGLOBAL dib = makeDib24(image); HBITMAP nativeBitmap = makeClipboardBitmap(image); - if (!png && !dibV5 && !dib && !nativeBitmap) return false; + HGLOBAL includeInHistory = serializedDword(1); + if (!png && !dibV5 && !dib && !nativeBitmap) { + if (includeInHistory) GlobalFree(includeInHistory); + return false; + } if (!openClipboardWithRetry(owner)) { if (png) GlobalFree(png); if (dibV5) GlobalFree(dibV5); if (dib) GlobalFree(dib); if (nativeBitmap) DeleteObject(nativeBitmap); + if (includeInHistory) GlobalFree(includeInHistory); return false; } if (!EmptyClipboard()) { @@ -247,6 +265,7 @@ bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) { if (dibV5) GlobalFree(dibV5); if (dib) GlobalFree(dib); if (nativeBitmap) DeleteObject(nativeBitmap); + if (includeInHistory) GlobalFree(includeInHistory); return false; } @@ -268,12 +287,17 @@ bool copyBitmapToClipboard(HWND owner, Gdiplus::Bitmap& bitmap) { nativeBitmap = nullptr; transferred = true; } + const UINT historyFormat = RegisterClipboardFormatW(L"CanIncludeInClipboardHistory"); + if (includeInHistory && historyFormat && SetClipboardData(historyFormat, includeInHistory)) { + includeInHistory = nullptr; + } CloseClipboard(); if (png) GlobalFree(png); if (dibV5) GlobalFree(dibV5); if (dib) GlobalFree(dib); if (nativeBitmap) DeleteObject(nativeBitmap); + if (includeInHistory) GlobalFree(includeInHistory); return transferred; } diff --git a/src/overlay.cpp b/src/overlay.cpp index 1e1e1e8..87244ed 100644 --- a/src/overlay.cpp +++ b/src/overlay.cpp @@ -141,19 +141,34 @@ Tool toolFromAction(Action action) { } } -void drawArrow(Graphics& g, const Pen& pen, PointF start, PointF end) { - g.DrawLine(&pen, start, end); +void drawArrow(Graphics& g, const Pen& pen, const Color& color, PointF start, PointF end) { const float dx = end.X - start.X; const float dy = end.Y - start.Y; - const float len = std::sqrt(dx * dx + dy * dy); - if (len < 1.0f) return; - const float angle = std::atan2(dy, dx); - const float head = std::clamp(len * 0.22f, 9.0f, 18.0f); - const float spread = 0.55f; - PointF a(end.X - head * std::cos(angle - spread), end.Y - head * std::sin(angle - spread)); - PointF b(end.X - head * std::cos(angle + spread), end.Y - head * std::sin(angle + spread)); - g.DrawLine(&pen, end, a); - g.DrawLine(&pen, end, b); + const float length = std::sqrt(dx * dx + dy * dy); + if (length < 1.0f) return; + + const float ux = dx / length; + const float uy = dy / length; + const float nx = -uy; + const float ny = ux; + const float stroke = std::max(1.0f, pen.GetWidth()); + const float headLength = std::clamp(stroke * 4.6f + 7.0f, 11.0f, 27.0f); + const float halfHeadWidth = std::clamp(headLength * 0.48f, 5.0f, 14.0f); + + // Lightshot-style arrow: one clean rounded shaft with a filled triangular + // head. The shaft runs under the head slightly so antialiasing never leaves + // a visible seam between the two shapes. + const float shaftInset = std::min(headLength * 0.58f, length * 0.45f); + PointF shaftEnd(end.X - ux * shaftInset, end.Y - uy * shaftInset); + g.DrawLine(&pen, start, shaftEnd); + + PointF baseCenter(end.X - ux * headLength, end.Y - uy * headLength); + PointF triangle[3]{ + end, + PointF(baseCenter.X + nx * halfHeadWidth, baseCenter.Y + ny * halfHeadWidth), + PointF(baseCenter.X - nx * halfHeadWidth, baseCenter.Y - ny * halfHeadWidth)}; + SolidBrush headBrush(color); + g.FillPolygon(&headBrush, triangle, 3); } void drawAnnotation(Graphics& g, const Annotation& a, float offsetX, float offsetY) { @@ -184,7 +199,7 @@ void drawAnnotation(Graphics& g, const Annotation& a, float offsetX, float offse if (a.points.size() >= 2) g.DrawLine(&pen, shifted(a.points[0]), shifted(a.points[1])); break; case Tool::Arrow: - if (a.points.size() >= 2) drawArrow(g, pen, shifted(a.points[0]), shifted(a.points[1])); + if (a.points.size() >= 2) drawArrow(g, pen, a.color, shifted(a.points[0]), shifted(a.points[1])); break; case Tool::Rectangle: if (a.points.size() >= 2) { @@ -231,7 +246,7 @@ void drawAnnotationClipped(Graphics& g, const Annotation& annotation, const RECT maxY = std::max(maxY, point.Y + offsetY); } float padding = annotation.width * 0.5f + 5.0f; - if (annotation.tool == Tool::Arrow) padding += 19.0f; + if (annotation.tool == Tool::Arrow) padding += 30.0f; if (annotation.tool == Tool::Text) { const float size = std::max(14.0f, annotation.textSize); maxX += std::max(size * 2.0f, static_cast(annotation.text.size()) * size * 0.72f); @@ -562,8 +577,8 @@ struct OverlayWindow::Impl { markerWidth = static_cast(stored.markerWidth); savedPalette = stored.paletteArgb; savedPaletteOccupied = stored.paletteOccupied; - color = color_picker_math::colorFromArgb(savedPalette[0]); - colorAlpha = color_picker_math::alphaFromArgb(savedPalette[0]); + color = color_picker_math::colorFromArgb(stored.selectedColorArgb); + colorAlpha = color_picker_math::alphaFromArgb(stored.selectedColorArgb); pickerHsv = color_picker_math::rgbToHsv(GetRValue(color), GetGValue(color), GetBValue(color)); POINT cursor{}; @@ -600,8 +615,6 @@ struct OverlayWindow::Impl { Tool tool = Tool::Drag; COLORREF color = theme::kAccent; std::vector annotations; - LONG appliedSelectionMoveDx = 0; - LONG appliedSelectionMoveDy = 0; std::vector