// ============================================================
// 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();
}
}
}