// ============================================================ // PdfDocument:PDF 文档封装(加载/页尺寸/按缩放渲染页位图) // 坐标系:页面坐标 = PDF 点(1/72 英寸),原点左上(pdfium 渲染后 // 已翻转为屏幕方向,y 向下)。 // ============================================================ using System; using System.Drawing; using System.IO; namespace pdfbt { /// 已打开的 PDF 文档。非线程安全,只在 UI 线程使用。 internal sealed class PdfDocument : IDisposable { private IntPtr _doc = IntPtr.Zero; private IntPtr[] _pageHandles; // 惰性加载的页句柄缓存 // ★ FPDF_LoadMemDocument 要求后备缓冲在文档关闭前保持有效且不可移动—— // 原来读入的 byte[] 是局部变量,打开后就没人引用;打开下一个 PDF 的大分配 // 触发 GC 把它搬走/回收,文档内存变垃圾 → 第二个 PDF 打不开(实测用户报障)。 // 钉住(GCHandleType.Pinned)+字段保引用,Close 时才释放。 private byte[] _bytes; private System.Runtime.InteropServices.GCHandle _pin; public string FilePath { get; private set; } public int PageCount { get; private set; } public float[] PageWidthPt { get; private set; } public float[] PageHeightPt { get; private set; } public bool IsOpen { get { return _doc != IntPtr.Zero; } } /// 打开 PDF(读入内存后走 FPDF_LoadMemDocument,免疫路径问题)。失败抛异常。 public void Open(string path, string password) { Close(); if (!File.Exists(path)) throw new Exception("文件不存在: " + path); _bytes = File.ReadAllBytes(path); _pin = System.Runtime.InteropServices.GCHandle.Alloc(_bytes, System.Runtime.InteropServices.GCHandleType.Pinned); IntPtr doc = PdfiumNative.LoadDocumentFromBytes(_bytes, password); if (doc == IntPtr.Zero) { FreeBuffer(); throw new Exception("PDF 打开失败:" + PdfiumNative.LastErrorText()); } int count = PdfiumNative.GetPageCount(doc); if (count <= 0) { PdfiumNative.CloseDocument(doc); FreeBuffer(); throw new Exception("PDF 没有任何页面。"); } _doc = doc; FilePath = path; PageCount = count; _pageHandles = new IntPtr[count]; PageWidthPt = new float[count]; PageHeightPt = new float[count]; for (int i = 0; i < count; i++) { float w, h; if (!PdfiumNative.GetPageSize(doc, i, out w, out h)) { w = 595f; h = 842f; // A4 兜底 } PageWidthPt[i] = w; PageHeightPt[i] = h; } } /// 释放文档后备缓冲(钉住句柄+字节数组)。 private void FreeBuffer() { if (_pin.IsAllocated) _pin.Free(); _bytes = null; } public void Close() { if (_pageHandles != null) { for (int i = 0; i < _pageHandles.Length; i++) PdfiumNative.ClosePage(_pageHandles[i]); _pageHandles = null; } if (_doc != IntPtr.Zero) { PdfiumNative.CloseDocument(_doc); _doc = IntPtr.Zero; } FreeBuffer(); FilePath = null; PageCount = 0; PageWidthPt = null; PageHeightPt = null; } /// 渲染指定页。scale = 像素/PDF点。失败返回 null。 public Bitmap RenderPage(int pageIndex, float scale) { if (!IsOpen) return null; if (pageIndex < 0 || pageIndex >= PageCount) return null; IntPtr page = GetPage(pageIndex); if (page == IntPtr.Zero) return null; int w = Math.Max(1, (int)Math.Round(PageWidthPt[pageIndex] * scale)); int h = Math.Max(1, (int)Math.Round(PageHeightPt[pageIndex] * scale)); return PdfiumNative.RenderPageToBitmap(page, w, h); } /// /// 高清渲染页面局部区域(OCR 用):与屏幕显示缩放无关, /// 用户任意缩放下框选,OCR 都拿到按 ocrScale 渲染的高清裁剪。 /// 返回位图与 rectPt 一一对应(像素/pt = ocrScale)。 /// public Bitmap RenderRegion(int pageIndex, RectangleF rectPt, float ocrScale) { if (!IsOpen) return null; if (pageIndex < 0 || pageIndex >= PageCount) return null; IntPtr page = GetPage(pageIndex); if (page == IntPtr.Zero) return null; // 整页渲染尺寸(控制缩放) int fullW = Math.Max(1, (int)Math.Round(PageWidthPt[pageIndex] * ocrScale)); int fullH = Math.Max(1, (int)Math.Round(PageHeightPt[pageIndex] * ocrScale)); // 区域矩形(裁到页面范围) float pw = PageWidthPt[pageIndex], ph = PageHeightPt[pageIndex]; float l = Math.Max(0, rectPt.X), t = Math.Max(0, rectPt.Y); float r = Math.Min(pw, rectPt.Right), b = Math.Min(ph, rectPt.Bottom); if (r - l < 1 || b - t < 1) return null; int startX = (int)Math.Floor(l * ocrScale); int startY = (int)Math.Floor(t * ocrScale); int endX = (int)Math.Ceiling(r * ocrScale); int endY = (int)Math.Ceiling(b * ocrScale); return PdfiumNative.RenderRegionToBitmap(page, startX, startY, endX - startX, endY - startY, fullW, fullH); } private IntPtr GetPage(int pageIndex) { IntPtr page = _pageHandles[pageIndex]; if (page == IntPtr.Zero) { page = PdfiumNative.LoadPage(_doc, pageIndex); _pageHandles[pageIndex] = page; } return page; } public void Dispose() { Close(); } } }