Complete Implementation Guide for PrintScreen v3.0
Phase 1: Project Setup and Dependencies
Step 1.1: Update Project Files
- Replace
vcpkg.jsonwith the new version (already provided) - Keep your
vcpkg-configuration.json(no changes needed) - Replace
CMakeLists.txtwith the updated version - Update baseline after saving files:
vcpkg x-update-baseline -c
Step 1.2: Install New Dependencies
# From your project root cmake --preset release # This will automatically download lodepng and libpng[apng]
Step 1.3: Create New Source Files
Create these new files in your project directory:
ApngWriter.h(provided artifact)ApngWriter.cpp(provided artifact)OptimizedGifWriter.h(provided artifact)OptimizedGifWriter.cpp(provided artifact)
Step 1.4: Update CMakeLists.txt to Include New Files
add_library(${PROJECT_NAME} SHARED plugin.cpp Config.cpp ScreenCapture.cpp PapyrusInterface.cpp ApngWriter.cpp # ADD THIS OptimizedGifWriter.cpp # ADD THIS printscreen.rc )
Phase 2: Modify Existing Header Files
Step 2.1: Update PCH.h
Add these includes to your precompiled header:
// Add to PCH.h #include <span> #include <optional> #include <queue> #include <condition_variable> #include <filesystem>
Step 2.2: Update Config.h
Add new configuration options:
// In Config.h, add to your configuration class: class Config { // ... existing members ... // New animation settings enum class AnimFormat { AUTO, GIF, APNG }; AnimFormat animatedFormat = AnimFormat::AUTO; bool enableOptimizations = true; bool prioritizeQuality = false; int apngLongModeThreshold = 5; // seconds // GIF optimizations bool gifUseDiffFrames = true; bool gifAutoDownscale2K = true; int gifMaxColors = 192; // Size limits int maxAnimatedDuration = 30; // seconds size_t warnFileSizeMB = 50; // warn if estimated > 50MB };
Step 2.3: Create AnimationCapture.h
Create a new header to manage animated captures:
#pragma once #include "ApngWriter.h" #include "OptimizedGifWriter.h" #include <memory> #include <thread> class AnimationCapture { public: enum class Format { GIF, APNG }; struct CaptureSettings { Format format; uint32_t width; uint32_t height; uint32_t durationSeconds; uint32_t fps; bool optimize; bool prioritizeQuality; std::wstring outputPath; }; bool StartCapture(const CaptureSettings& settings); void AddFrame(const uint8_t* bgraData, size_t dataSize); bool StopCapture(std::wstring* error = nullptr); size_t GetEstimatedFileSize() const; bool IsCapturing() const { return isCapturing_; } private: std::unique_ptr<IApngWriter> apngWriter_; std::unique_ptr<IGifWriter> gifWriter_; std::thread workerThread_; Format currentFormat_; bool isCapturing_ = false; uint32_t captureWidth_ = 0; uint32_t captureHeight_ = 0; void ProcessFrames(); };
Phase 3: Modify ScreenCapture.cpp
Step 3.1: Add Animation Support to ScreenCapture Class
// In ScreenCapture.cpp, add to class private members: private: // ... existing members ... // Animation capture std::unique_ptr<AnimationCapture> animCapture_; bool isAnimatedCapture_ = false; std::chrono::steady_clock::time_point animStartTime_; uint32_t animDurationSeconds_ = 0; uint32_t animFrameCount_ = 0;
Step 3.2: Add New Capture Methods
// Add these methods to ScreenCapture class: bool ScreenCapture::StartAnimatedCapture( const std::wstring& outputPath, const std::string& format, uint32_t durationSeconds) { if (isAnimatedCapture_) { return false; // Already capturing } // Get current screen dimensions RECT desktop; GetWindowRect(GetDesktopWindow(), &desktop); uint32_t width = desktop.right - desktop.left; uint32_t height = desktop.bottom - desktop.top; // Setup animation capture animCapture_ = std::make_unique<AnimationCapture>(); AnimationCapture::CaptureSettings settings; settings.width = width; settings.height = height; settings.durationSeconds = durationSeconds; settings.fps = 10; // Default 10 FPS settings.optimize = Config::GetSingleton()->enableOptimizations; settings.prioritizeQuality = Config::GetSingleton()->prioritizeQuality; settings.outputPath = outputPath; // Determine format if (format == "APNG") { settings.format = AnimationCapture::Format::APNG; } else if (format == "GIF") { settings.format = AnimationCapture::Format::GIF; } else { // Auto-select based on duration and resolution bool is2K = (width >= 1920 && height >= 1080); if (durationSeconds > 10 || is2K) { settings.format = AnimationCapture::Format::APNG; } else { settings.format = AnimationCapture::Format::GIF; } } if (!animCapture_->StartCapture(settings)) { animCapture_.reset(); return false; } isAnimatedCapture_ = true; animStartTime_ = std::chrono::steady_clock::now(); animDurationSeconds_ = durationSeconds; animFrameCount_ = 0; // Start capture timer StartAnimationTimer(); return true; } void ScreenCapture::CaptureAnimationFrame() { if (!isAnimatedCapture_ || !animCapture_) { return; } // Check if duration exceeded auto elapsed = std::chrono::steady_clock::now() - animStartTime_; auto elapsedSeconds = std::chrono::duration_cast<std::chrono::seconds>(elapsed).count(); if (elapsedSeconds >= animDurationSeconds_) { StopAnimatedCapture(); return; } // Capture frame using existing Desktop Duplication ComPtr<IDXGISurface> surface; if (SUCCEEDED(CaptureDesktop(&surface))) { // Map surface D3D11_MAPPED_SUBRESOURCE mapped; if (SUCCEEDED(context_->Map(surface.Get(), 0, D3D11_MAP_READ, 0, &mapped))) { // Add frame to animation animCapture_->AddFrame( static_cast<uint8_t*>(mapped.pData), mapped.RowPitch * desc.Height ); context_->Unmap(surface.Get(), 0); animFrameCount_++; } } } bool ScreenCapture::StopAnimatedCapture() { if (!isAnimatedCapture_ || !animCapture_) { return false; } isAnimatedCapture_ = false; std::wstring error; bool success = animCapture_->StopCapture(&error); if (!success) { logger::error("Failed to finalize animation: {}", std::string(error.begin(), error.end())); } else { logger::info("Animation capture complete: {} frames", animFrameCount_); } animCapture_.reset(); return success; }
Step 3.3: Modify Existing Capture Loop
// In your existing capture method, add animation frame capture: void ScreenCapture::CaptureLoop() { // ... existing code ... // Add this check in your main capture loop if (isAnimatedCapture_) { // Check frame timing (for 10 FPS) static auto lastFrameTime = std::chrono::steady_clock::now(); auto now = std::chrono::steady_clock::now(); auto frameInterval = std::chrono::milliseconds(100); // 10 FPS if (now - lastFrameTime >= frameInterval) { CaptureAnimationFrame(); lastFrameTime = now; } } // ... rest of existing code ... }
Phase 4: Update PapyrusInterface.cpp
Step 4.1: Add New Papyrus Functions
// In PapyrusInterface.cpp, add these new native functions: bool PapyrusInterface::CaptureAnimatedPNG( RE::StaticFunctionTag*, std::string outputPath, int32_t durationSeconds) { if (durationSeconds <= 0 || durationSeconds > 30) { logger::warn("Invalid duration: {}", durationSeconds); return false; } auto capture = ScreenCapture::GetSingleton(); std::wstring widePath = StringToWString(outputPath); if (widePath.find(L".png") == std::wstring::npos) { widePath += L".png"; } return capture->StartAnimatedCapture(widePath, "APNG", durationSeconds); } bool PapyrusInterface::CaptureOptimizedGIF( RE::StaticFunctionTag*, std::string outputPath, int32_t durationSeconds, bool prioritizeQuality) { if (durationSeconds <= 0 || durationSeconds > 30) { logger::warn("Invalid duration: {}", durationSeconds); return false; } // Temporarily set quality preference auto config = Config::GetSingleton(); bool oldPriority = config->prioritizeQuality; config->prioritizeQuality = prioritizeQuality; auto capture = ScreenCapture::GetSingleton(); std::wstring widePath = StringToWString(outputPath); if (widePath.find(L".gif") == std::wstring::npos) { widePath += L".gif"; } bool result = capture->StartAnimatedCapture(widePath, "GIF", durationSeconds); // Restore setting config->prioritizeQuality = oldPriority; return result; } std::string PapyrusInterface::GetRecommendedFormat( RE::StaticFunctionTag*, int32_t width, int32_t height, int32_t durationSeconds) { bool is2K = (width >= 1920 && height >= 1080); if (durationSeconds > 15 || is2K) { return "APNG"; } else if (durationSeconds <= 5) { return "GIF"; } else { // Return both as options return "GIF or APNG"; } } int32_t PapyrusInterface::GetEstimatedFileSize( RE::StaticFunctionTag*, std::string format, int32_t width, int32_t height, int32_t durationSeconds) { if (format == "APNG") { // APNG estimation size_t pixels = width * height * 4; size_t frames = durationSeconds * 10; size_t raw = pixels * frames; return (int32_t)(raw * 0.15 / 1024); // KB } else { // GIF estimation with optimizations auto suggestion = GifOptimizer::AnalyzeCapture( width, height, durationSeconds, true ); int finalWidth = suggestion.should_downscale ? (int)(width * suggestion.scale_factor) : width; int finalHeight = suggestion.should_downscale ? (int)(height * suggestion.scale_factor) : height; return (int32_t)(GifOptimizer::EstimateFileSize( finalWidth, finalHeight, durationSeconds, suggestion.recommended_fps, suggestion.recommended_colors, true ) / 1024); // KB } }
Step 4.2: Register New Functions
// In RegisterFunctions method: bool PapyrusInterface::RegisterFunctions(RE::BSScript::IVirtualMachine* vm) { // ... existing registrations ... // New animation functions vm->RegisterFunction("CaptureAnimatedPNG", "PrintScreen", CaptureAnimatedPNG); vm->RegisterFunction("CaptureOptimizedGIF", "PrintScreen", CaptureOptimizedGIF); vm->RegisterFunction("GetRecommendedFormat", "PrintScreen", GetRecommendedFormat); vm->RegisterFunction("GetEstimatedFileSize", "PrintScreen", GetEstimatedFileSize); return true; }
Phase 5: Create AnimationCapture.cpp
Step 5.1: Implement AnimationCapture Class
// Create AnimationCapture.cpp #include "AnimationCapture.h" #include "ApngWriter.h" #include "OptimizedGifWriter.h" #include <chrono> bool AnimationCapture::StartCapture(const CaptureSettings& settings) { if (isCapturing_) { return false; } currentFormat_ = settings.format; captureWidth_ = settings.width; captureHeight_ = settings.height; if (currentFormat_ == Format::APNG) { // Create APNG writer apngWriter_ = CreateApngWriter( settings.durationSeconds, settings.width, settings.height ); ApngParams params; params.width = settings.width; params.height = settings.height; params.delay_num = 1; params.delay_den = settings.fps; params.use_diff_rect = settings.optimize; params.zlib_level = settings.prioritizeQuality ? 6 : 3; uint32_t expectedFrames = settings.durationSeconds * settings.fps; if (!apngWriter_->begin(settings.outputPath, params, expectedFrames)) { apngWriter_.reset(); return false; } } else { // Create GIF writer GifWriterFactory::AutoConfig config; config.duration_seconds = settings.durationSeconds; config.width = settings.width; config.height = settings.height; config.prioritize_quality = settings.prioritizeQuality; gifWriter_ = GifWriterFactory::Create(config); GifParams params = GifWriterFactory::GetOptimalParams(config); uint32_t expectedFrames = settings.durationSeconds * params.fps; if (!gifWriter_->begin(settings.outputPath, params, expectedFrames)) { gifWriter_.reset(); return false; } } isCapturing_ = true; // Start worker thread workerThread_ = std::thread([this]() { ProcessFrames(); }); return true; } void AnimationCapture::AddFrame(const uint8_t* bgraData, size_t dataSize) { if (!isCapturing_) { return; } // Convert BGRA to RGBA std::vector<uint8_t> rgbaBuffer(dataSize); size_t pixelCount = dataSize / 4; // Use the utility function from ApngWriter.h apngutil::Unpremultiply_BGRA_to_RGBA(bgraData, rgbaBuffer.data(), pixelCount); // Queue for processing { std::lock_guard<std::mutex> lock(queueMutex_); frameQueue_.push(std::move(rgbaBuffer)); } queueCV_.notify_one(); } bool AnimationCapture::StopCapture(std::wstring* error) { if (!isCapturing_) { return false; } isCapturing_ = false; // Signal worker to stop { std::lock_guard<std::mutex> lock(queueMutex_); stopProcessing_ = true; } queueCV_.notify_one(); // Wait for worker if (workerThread_.joinable()) { workerThread_.join(); } // Finalize bool result = false; if (apngWriter_) { result = apngWriter_->finalize(error); apngWriter_.reset(); } else if (gifWriter_) { result = gifWriter_->finalize(error); gifWriter_.reset(); } return result; } void AnimationCapture::ProcessFrames() { // Frame processing loop while (true) { std::vector<uint8_t> frame; { std::unique_lock<std::mutex> lock(queueMutex_); queueCV_.wait(lock, [this] { return !frameQueue_.empty() || stopProcessing_; }); if (stopProcessing_ && frameQueue_.empty()) { break; } if (!frameQueue_.empty()) { frame = std::move(frameQueue_.front()); frameQueue_.pop(); } } if (!frame.empty()) { if (apngWriter_) { apngWriter_->addFrame(frame, std::nullopt); } else if (gifWriter_) { gifWriter_->addFrame(frame, std::nullopt); } } } }
Phase 6: Update Papyrus Scripts
Step 6.1: Update Your Papyrus Script
; In your PrintScreen.psc script, add new functions: ; Capture animated PNG Function CaptureAnimatedPNG(string outputPath, int duration) native global ; Capture optimized GIF Function CaptureOptimizedGIF(string outputPath, int duration, bool quality) native global ; Get recommended format string Function GetRecommendedFormat(int width, int height, int duration) native global ; Get estimated file size in KB int Function GetEstimatedFileSize(string format, int width, int height, int duration) native global ; Wrapper function for MCM Function CaptureAnimated(string format, int duration) string path = GetOutputPath() if format == "APNG" CaptureAnimatedPNG(path, duration) elseif format == "GIF" CaptureOptimizedGIF(path, duration, false) else ; Auto-select string recommended = GetRecommendedFormat(1920, 1080, duration) if recommended == "APNG" CaptureAnimatedPNG(path, duration) else CaptureOptimizedGIF(path, duration, false) endif endif EndFunction
Phase 7: Testing and Validation
Step 7.1: Build the Project
# Clean build cmake --build build/release --clean-first
Step 7.2: Test Each Component
- Test APNG capture at various durations (5s, 15s, 30s)
- Test GIF optimization with 2K content
- Verify memory usage stays under limits
- Check file sizes match estimates
- Test format auto-selection
Step 7.3: Performance Testing
// Add performance logging logger::info("Frame {} processed in {}ms", frameCount, processingTime.count());
Phase 8: Final Integration Checklist
- [ ] All new files added to project
- [ ] CMakeLists.txt updated with new sources
- [ ] vcpkg dependencies installed
- [ ] Existing capture code modified
- [ ] Papyrus interface updated
- [ ] Animation classes implemented
- [ ] Worker threads properly managed
- [ ] Memory cleanup verified
- [ ] Error handling complete
- [ ] Logging added for debugging
- [ ] MCM menu updated (if applicable)
- [ ] Documentation updated
Common Issues and Solutions
Issue: Link errors with PNG/LodePNG
Solution: Ensure vcpkg installed correctly:
vcpkg install lodepng:x64-windows-static vcpkg install libpng[apng]:x64-windows-static
Issue: Animation capture crashes
Solution: Check worker thread synchronization and ensure proper cleanup in destructors
Issue: Large memory usage
Solution: Verify streaming mode activates for long captures (>10s)
Issue: Poor GIF quality
Solution: Adjust dithering strength and color count in GifParams
Build Command Summary
# Full rebuild with new features cd /path/to/PrintScreen vcpkg x-update-baseline -c cmake --preset release cmake --build build/release --clean-first
This completes the full integration of animated PNG and optimized GIF support into PrintScreen v3.0.