找回密码
 立即注册
首页 业界区 业界 从零开始: C#轻松预览PDF文件-支持跨平台AOT友好 ...

从零开始: C#轻松预览PDF文件-支持跨平台AOT友好

梦霉 前天 16:40
本项目对PdfiumViewer库进行了改写,对其pdf解析部分的核心功能进行了分离和精简,使其支持任意程序调用生成渲染后图片,项目代码已全部开源 (https://github.com/LdotJdot/LumPdfiumViewerSlim)。
同时我们还给出了一个用Avalonia简单实现了渲染页面的UI,改造后的库是完全支持如Winform还有后端调用的。
1.gif

下文将分享一下这个过程。
一、基于PDFium库

需求来了,就得想办法实现诶。要预览PDF文件,需要先完成对PDF的解析,即将PDF中各类数据提取出来,然后再实现对解析数据的绘制渲染到软件层面。在C#中关于PDF解析库有很多,我们优先考虑的主要为用C#对PDFium封装库。PDFium是基于由google维护的项目 (https://github.com/chromium/pdfium) (Apache-2.0 license),维护很频繁(本文发出前2小时Pdfium仓库还在更新),此外他的特点是采用动态链接库,C++编译体积很小5mb,兼容性较好,接口规范而且功能强大。
二、C#现有Pdf预览库PdfiumViewer的不便

为了不造轮子,我们找到了 PdfiumViewer (https://github.com/bezzad/PdfiumViewer,Apache-2.0 license), 是一个基于Pdfium封装后实现了预览效果的C#库。
尽管PDFium预览效果很好,但最大的问题是,PdfiumViewer中对渲染后页面的创建深度依赖了WPF框架(true),而且是与解析渲染方法组合在一起的。由于很多内部对象大量引用了WPF元素。因此在后端调用时或用于其他如Winform、Avalonia框架时非常不方便。如果想要单纯集成至Winform或后端服务器调用那么还必须带着WPF框架过去,非常不方便,只能自定义对PdfiumViewer进行改造了。
三、Pdf预览原理检视及修改

Pdf的预览主要是由解析+渲染两部分实现。PdfiumViewer中,解析和底层渲染都是基于Pdfium库的封装调用实现的。如:,
  1. [DllImport("pdfium.dll")]
  2. public static extern IntPtr FPDFBitmap_CreateEx(int width, int height, int format, IntPtr first_scan, int stride);
  3. [DllImport("pdfium.dll")]
  4. public static extern void FPDFBitmap_FillRect(IntPtr bitmapHandle, int left, int top, int width, int height, uint color);
  5. [DllImport("pdfium.dll")]
  6. public static extern void FPDF_RenderPageBitmap(IntPtr bitmapHandle, IntPtr page, int start_x, int start_y, int size_x, int size_y, int rotate, FPDF flags);
复制代码
在PdfiumView封装的方法中,将额外考虑:

  • 状态检查和DPI校正
  • 位图对象管理及内存锁定
  • 原生PDF渲染 (Pdfium中渲染准备)
  • 背景设置和页面设置
  • 实际PDF渲染 (渲染到位图)
PdfiumView封装后的方法为:
  1. public Image Render(int page, int width, int height, float dpiX, float dpiY, PdfRotation rotate, PdfRenderFlags flags)
复制代码
基于这个方法仅需要传入页码(从0开始)、宽度、高度、dpi、旋转等参数,就可以得到PDF文件中指定页的渲染后图片了。
四、精简处理及Avalonia页面实现

为了更加通用,我们对PdfiumViewer中依赖WPF的代码进行了删减,主要包括书签相关的对象,滚动面板及动态渲染等,最后留下的只有指定PDF页渲染图片的功能。在大部分场景中已经够用了,而且是支持AOT发布的,也可以作为一个独立进程工具供其他程序调用。
为了测试效果,我们选择在Avalonia中创建一个可滚动的页面,创建一个虚拟模式的ItemsControl,基于Image对象用于显示最终渲染的图片页IImage:
  1. <ScrollViewer>
  2.         <ItemsControl  x:Name="pageList"
  3.      IsTabStop="False"
  4.                 Focusable="False"
  5.                 IsHitTestVisible="True"
  6.                 ItemsSource="{Binding RenderedPages.DisplayedData, Mode=OneWay}" Background="Transparent">
  7.                 <ItemsControl.ItemsPanel>
  8.                         <ItemsPanelTemplate>
  9.                                 <VirtualizingStackPanel/>
  10.                         </ItemsPanelTemplate>
  11.                 </ItemsControl.ItemsPanel>
  12.                
  13.                 <ItemsControl.ItemTemplate>
  14.                        
  15.                         <DataTemplate>
  16.                                 <Image
  17.                                         Source="{Binding Image}"
  18.                                            Stretch="Uniform"/>
  19.                         </DataTemplate>
  20.                 </ItemsControl.ItemTemplate>
  21.         </ItemsControl >
  22. </ScrollViewer>
复制代码
绑定的ViewModel很简单,主要是为了页面的延迟渲染,即滚动到哪一页,就调用上述组件的方法,实时渲染当前页图片。
``csharp
  1. public interface IPageRender
  2. {
  3.     public int page { get; }
  4.     public IImage Image {get;}
  5. }
  6. public class PageRender
  7. {
  8.     PdfDocument _pdfDocument;
  9.     public PageRender(PdfDocument _pdfDocument, int pageNumber)
  10.     {
  11.         this._pdfDocument= _pdfDocument;
  12.         this.page = pageNumber;
  13.     }
  14.     public IImage Image=>GetImage();
  15.    IImage GetImage()
  16.     {
  17.         try
  18.         {
  19.             using var image = _pdfDocument.Render(page, 800, 1200, 192, 192);
  20.                         
  21.             return ConvertToAvaloniaBitmap(image);
  22.         }
  23.         catch
  24.         {
  25.             return null;
  26.         }
  27.     }
  28.     private Avalonia.Media.Imaging.Bitmap ConvertToAvaloniaBitmap(System.Drawing.Image image)
  29.     {
  30.         using (var memoryStream = new MemoryStream())
  31.         {               
  32.             image.Save(memoryStream,ImageFormat.Png);
  33.             memoryStream.Position = 0;
  34.             return new Avalonia.Media.Imaging.Bitmap(memoryStream);
  35.         }
  36.     }
  37.     public int page { get; }
  38. }
  39. public class PageViewModel: ReactiveObject,IDisposable
  40. {
  41.     private PdfDocument _pdfDocument;
  42.     IEnumerable<PageRender> _displayedData=[];
  43.     public IEnumerable<PageRender> DisplayedData
  44.     {
  45.         get => _displayedData;
  46.         private set => this.RaiseAndSetIfChanged(ref _displayedData, value);
  47.     }
  48.     public void Load(string path)
  49.     {
  50.         _pdfDocument?.Dispose();
  51.         _pdfDocument = PdfDocument.Load(path);
  52.         Initialize(_pdfDocument);
  53.     }
  54.     private void Initialize(PdfDocument pdfDocument)
  55.     {
  56.         _pdfDocument = pdfDocument;
  57.         // 页面范围
  58.         DisplayedData = Enumerable.Range(0, pdfDocument.PageCount).Select(o=>new PageRender(_pdfDocument, o));
  59.     }
  60.     public void Dispose()
  61.     {            
  62.         _pdfDocument?.Dispose();
  63.     }
  64. }
复制代码
  1. 实现效果如下:
  2. ![pdfPreview2](https://img2024.cnblogs.com/blog/3529796/202510/3529796-20251023154908374-359808541.png)
  3. ## 五、最后
  4. 尽管AOT启动速度快,但我们还是更偏向与单文件的压缩发布,因为AOT发布后,加上非托管库,体积会比较大。而单文件压缩将非托管后一起打包后大小仅不到26Mb,一个小巧的PDF阅读器就诞生了,是不是很棒。
  5. 如果你对本文建议或想法,欢迎随时交流。请关注我们的公众号`萤火初芒`,以后会和大家分享更多有趣内容,一起学习交流进步。项目代码已全部开源 (https://github.com/LdotJdot/LumPdfiumViewerSlim),欢迎给个星星。
  6. ![QR](https://img2024.cnblogs.com/blog/3529796/202509/3529796-20250915150401305-1035823235.jpg)
复制代码
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册