Complete Implementation Guide for PrintScreen v3.0

Phase 1: Project Setup and Dependencies

Step 1.1: Update Project Files

  1. Replace vcpkg.json with the new version (already provided)
  2. Keep your vcpkg-configuration.json (no changes needed)
  3. Replace CMakeLists.txt with the updated version
  4. 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

  1. Test APNG capture at various durations (5s, 15s, 30s)
  2. Test GIF optimization with 2K content
  3. Verify memory usage stays under limits
  4. Check file sizes match estimates
  5. 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.