using System; using System.ComponentModel; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Windows; using System.Windows.Media.Imaging; using Docnet.Core; using Docnet.Core.Models; namespace KillerPDF { /// /// ViewModel for a single page thumbnail in the sidebar PageList. /// Thumbnail is loaded lazily on a background thread; the UI binds to /// the property and updates via PropertyChanged. /// internal sealed class PageThumbnailVm(int pageIndex, string filePath, int rotation = 0) : INotifyPropertyChanged { // Limit concurrent pdfium doc-reader opens to avoid contention private static readonly SemaphoreSlim _loadSem = new(2, 2); private BitmapSource? _thumb; private bool _loadRequested; public int PageIndex { get; } = pageIndex; public string Label => string.Format( Application.Current?.TryFindResource("Str_PageLabel") as string ?? "Page {0}", PageIndex + 1); private readonly string _filePath = filePath; private readonly int _rotation = ((rotation % 360) + 360) % 360; // degrees: 0, 90, 180, 270 public BitmapSource? Thumbnail => _thumb; public event PropertyChangedEventHandler? PropertyChanged; /// Called when the ListBox item becomes visible (via binding getter trigger). public void RequestLoad() { if (_loadRequested) return; _loadRequested = true; System.Threading.Tasks.Task.Run(LoadAsync); } /// /// Seed the thumbnail before ItemsSource is set - no dispatch needed because /// no binding exists yet. Used to carry old thumbnails across a RefreshPageList /// call so the list never flashes blank. /// internal void SetThumbnailDirect(BitmapSource src) => _thumb = src; /// Called by RefreshPageList's bulk background loader. internal void SetThumbnail(BitmapSource src) { // A background load can finish after the app has begun shutting down (or between // tab switches), when Application.Current is briefly null. The UI is going away in // that case, so just drop the update instead of throwing. var app = Application.Current; if (app == null) return; app.Dispatcher.BeginInvoke(new Action(() => { _thumb = src; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Thumbnail))); })); } private async System.Threading.Tasks.Task LoadAsync() { await _loadSem.WaitAsync().ConfigureAwait(false); try { var src = BuildThumb(_filePath, PageIndex, _rotation); if (src != null) SetThumbnail(src); } catch { /* thumbnail not critical */ } finally { _loadSem.Release(); } } internal static BitmapSource? BuildThumb(string filePath, int pageIndex, int rotation = 0) { try { // Render thumbnails at a higher resolution than they're usually shown so the page list // stays crisp when the sidebar is dragged wider (thumbnails scale to the sidebar width). // 288px covers the ~240px the page can show at the widest sidebar; raise for sharper, // lower to save memory (each loaded thumbnail is kept in RAM). using var docReader = DocLib.Instance.GetDocReader(filePath, new PageDimensions(288, 576)); using var pr = docReader.GetPageReader(pageIndex); int tw = pr.GetPageWidth(); int th = pr.GetPageHeight(); var raw = KillerPDF.Services.PdfiumInterop.RenderPageWithAnnotations(filePath, pageIndex, tw, th) ?? pr.GetImage(); // #141 if (tw <= 0 || th <= 0 || raw == null || raw.Length < tw * th * 4) return null; // Apply in-memory rotation (temp file stores /Rotate=0; _pageRotations holds true angle) if (rotation != 0) (raw, tw, th) = Services.BitmapHelpers.RotateBitmap(raw, tw, th, rotation); return EncodeToBitmapSource(raw, tw, th); } catch { return null; } } /// /// Encode already-decoded BGRA pixels (with rotation applied) to a frozen BitmapFrame. /// Called by the RefreshPageList bulk loader which manages its own doc reader. /// internal static BitmapSource? BuildThumbFromRaw(byte[] bgra, int width, int height) => EncodeToBitmapSource(bgra, width, height); /// /// Encode raw BGRA (pdfium) → PNG → frozen BitmapFrame entirely on the calling thread. /// GDI+ Format32bppArgb is BGRA in memory, matching pdfium output exactly. /// private static BitmapSource? EncodeToBitmapSource(byte[] bgra, int width, int height) { var pin = GCHandle.Alloc(bgra, GCHandleType.Pinned); try { using var bmp = new System.Drawing.Bitmap( width, height, width * 4, System.Drawing.Imaging.PixelFormat.Format32bppArgb, pin.AddrOfPinnedObject()); using var ms = new MemoryStream(); bmp.Save(ms, System.Drawing.Imaging.ImageFormat.Png); ms.Position = 0; var src = BitmapFrame.Create(ms, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); src.Freeze(); return src; } finally { pin.Free(); } } } }