Spire.pdf 等打印 pdf 的库需要付费,因此本篇文章是通过使用 Google 开源的 PDFium 项目实现打印, PDFium 项目开源且跨平台。
bblanchon.PDFium.Win32 则是一个使用 C# 封装了 PDFium 的库。
引入三个库:
<ItemGroup>
<PackageReference Include="bblanchon.PDFium.Win32" Version="122.0.6259" />
<PackageReference Include="PdfiumPrinter" Version="1.4.1" />
<PackageReference Include="System.Drawing.Common" Version="7.0.0" />
<PackageReference Include="Vanara.PInvoke.Printing" Version="3.4.17" />
</ItemGroup>
理论上,如果使用了 bblanchon.PDFium.Win32 ,理论上会自动携带 PDFium 动态库,示例:
bblanchon.PDFium 还有其它系统下的封装库,会自动带上对应系统的 PDFium 动态库。
正常情况,编译项目之后,会自动在目录中带出这些动态库, 不过有些情况下,使用 Release 发布项目不会带出这些动态库。例如程序设置了框架为 net8.0-windows
或者编译的时候指定了系统和 CPU 架构,例如 dotnet publish -c Release -r win-x64
。
这个时候编译出来的程序是不会带出动态库的。
可以手动到这个仓库下载编译好的动态库:https://github.com/bblanchon/pdfium-binaries
然后手动放到项目的目录下:
├─runtimes
│ ├─linux-arm
│ │ └─native
│ │ libpdfium.so
│ │
│ ├─linux-arm64
│ │ └─native
│ │ libpdfium.so
│ │
│ ├─linux-musl-arm64
│ │ └─native
│ │ libpdfium.so
│ │
│ ├─linux-musl-x64
│ │ └─native
│ │ libpdfium.so
│ │
│ ├─linux-musl-x86
│ │ └─native
│ │ libpdfium.so
│ │
│ ├─linux-x64
│ │ └─native
│ │ libpdfium.so
│ │
│ ├─linux-x86
│ │ └─native
│ │ libpdfium.so
│ │
│ ├─win-arm64
│ │ └─native
│ │ pdfium.dll
│ │
│ ├─win-x64
│ │ └─native
│ │ pdfium.dll
│ │
│ └─win-x86
│ └─native
│ pdfium.dll
并不需要所以动态库都使用,比如我只需要在 win x64 下使用,则只需要复制这个文件到项目下面:
下面开始讲解代码。
定义两个传递配置的模型类:
/// <summary>
/// 打印机配置
/// </summary>
public class PrintOption
{
/// <summary>
/// 打印机名称<br />
/// <para>如果为空,则使用默认打印机</para>
/// </summary>
public string? PrinterName { get; set; }
/// <summary>
/// 是否自动打印,即静默打印。
/// <para>默认使用静默打印。</para>
/// </summary>
public bool IsAutoPrint { get; set; } = true;
/// <summary>
/// 是否彩色打印
/// </summary>
public bool? Color { get; set; }
/// <summary>
/// 页边距
/// </summary>
public Margins? Margins { get; set; }
/// <summary>
/// 打印纸张大小名称。
/// <para><see cref="PaperName"/> 跟 <see cref="CustomSize"/> 二选一,<see cref="CustomSize"/> 优先级高。</para>
/// </summary>
public string? PaperName { get; set; }
/// <summary>
/// 自定义纸张大小。
/// <para><see cref="PaperName"/> 跟 <see cref="CustomSize"/> 二选一,<see cref="CustomSize"/> 优先级高</para>
/// </summary>
public Size? CustomSize { get; set; }
/// <summary>
/// 打印方向设置为横向。
/// </summary>
public bool? Landscape { get; set; }
/// <summary>
/// 要打印多少份,默认为 1 份。
/// </summary>
public short Count { get; set; } = 1;
}
public class PrintImageOption : PrintOption
{
/// <summary>
/// 用于指定在图像缩放或变换时使用的插值算法。
/// <para>Mode 和 Dpi 不冲突</para>
/// </summary>
/// <remarks>
/// <see cref="InterpolationMode.Default"/> 使用默认的插值模式。通常为Bilinear。<br />
/// <see cref="InterpolationMode.Low"/>: 低质量的插值模式,用于快速处理较大的图像。<br />
/// <see cref="InterpolationMode.High"/>: 高质量的插值模式,用于确保在图像缩放或变换时获得更好的细节和平滑度。<br />
/// <see cref="InterpolationMode.Bilinear"/>: 双线性插值模式,以平均周围4个像素的颜色来计算新像素的颜色值。<br />
/// <see cref="InterpolationMode.Bicubic"/>: 双三次插值模式,以周围16个像素的颜色加权平均来计算新像素的颜色值。<br />
/// <see cref="InterpolationMode.NearestNeighbor"/>: 最近邻插值模式,使用与目标像素最接近的原始像素的颜色值。<br />
/// <see cref="InterpolationMode.HighQualityBilinear"/>: 高质量双线性插值模式,类似于Bilinear,但具有更好的质量。<br />
/// <see cref="InterpolationMode.HighQualityBicubic"/>: 高质量双三次插值模式,类似于Bicubic,但具有更好的质量。<br />
/// </remarks>
public InterpolationMode? Mode { get; set; }
/// <summary>
/// 分辨率,默认打印机 dpi 96,dpi 影响打印机打印的物理成像。
/// <para>Mode 和 Dpi 不冲突</para>
/// </summary>
public int? Dpi { get; set; } = 300;
/// <summary>
/// 自动缩放,如果图片过大,则会自动缩小;如果图片过小,则会自动放大。
/// </summary>
public bool IsAutoScale { get; set; } = true;
}
定义从 PrintOption 配置整理到打印机设置的函数。
private static void BuildOption(PrintDocument pd, PrintOption printOption)
{
if (printOption == null) return;
// 设置打印机名称
if (!string.IsNullOrEmpty(printOption.PrinterName))
{
pd.PrinterSettings.PrinterName = printOption.PrinterName;
pd.DefaultPageSettings.PrinterSettings.PrinterName = printOption.PrinterName;
}
// 是否静默打印
if (printOption.IsAutoPrint)
{
pd.PrintController = new StandardPrintController();
}
// 打印份数
pd.PrinterSettings.Copies = printOption.Count;
// 是否彩色打印
if (printOption.Color != null && pd.PrinterSettings.SupportsColor)
{
pd.PrinterSettings.DefaultPageSettings.Color = printOption.Color.GetValueOrDefault();
pd.DefaultPageSettings.Color = printOption.Color.GetValueOrDefault();
}
// 是否横向打印
if (printOption.Landscape != null)
{
pd.PrinterSettings.DefaultPageSettings.Landscape = printOption.Landscape.GetValueOrDefault();
pd.DefaultPageSettings.Landscape = printOption.Landscape.GetValueOrDefault();
}
// 设置页边距
if (printOption.Margins != null)
{
pd.PrinterSettings.DefaultPageSettings.Margins = printOption.Margins;
pd.DefaultPageSettings.Margins = printOption.Margins;
}
// 设置纸张大小
if (printOption.CustomSize != null)
{
var paper = new PaperSize("custom", printOption.CustomSize.Value.Width, printOption.CustomSize.Value.Height)
{
PaperName = "custom",
RawKind = (int)PaperKind.Custom
};
pd.PrinterSettings.DefaultPageSettings.PaperSize = paper;
pd.DefaultPageSettings.PaperSize = paper;
}
else if (printOption.PaperName != null)
{
for (int i = 0; i < pd.PrinterSettings.PaperSizes.Count; i++)
{
if (pd.PrinterSettings.PaperSizes[i].PaperName == printOption.PaperName)
{
var paper = pd.PrinterSettings.PaperSizes[i];
pd.PrinterSettings.DefaultPageSettings.PaperSize = new PaperSize(paper.PaperName, paper.Width, paper.Height);
pd.DefaultPageSettings.PaperSize = new PaperSize(paper.PaperName, paper.Width, paper.Height);
break;
}
}
}
}
打印文字:
public static void PrintText(string[] text, PrintOption? printOption)
{
if (printOption == null) printOption = new PrintOption();
PrintDocument pd = new PrintDocument();
BuildOption(pd, printOption);
pd.PrintPage += PrintTxt;
pd.Print();
void PrintTxt(object sender, PrintPageEventArgs ev)
{
var printFont = new System.Drawing.Font(System.Drawing.SystemFonts.DefaultFont.Name, System.Drawing.SystemFonts.DefaultFont.Size);
float linesPerPage = 0;
float yPos = 0;
int count = 0;
float leftMargin = ev.MarginBounds.Left;
float topMargin = ev.MarginBounds.Top;
string line = string.Empty;
// 计算高度,一页能够打印多少行
linesPerPage = ev.MarginBounds.Height / printFont.GetHeight(ev.Graphics!);
int index = 0;
// 打印每一行
while (count < linesPerPage && index < text.Length)
{
line = text[index];
yPos = topMargin + (count * printFont.GetHeight(ev.Graphics!));
ev.Graphics!.DrawString(line, printFont, Brushes.Black, leftMargin, yPos, new StringFormat());
count++;
index++;
}
if (string.IsNullOrEmpty(line))
ev.HasMorePages = true;
else
ev.HasMorePages = false;
}
}
打印图片:
public static void PrintImage(Stream[] streams, PrintImageOption? printOption)
{
if (printOption == null) printOption = new PrintImageOption();
PrintDocument pd = new PrintDocument();
BuildOption(pd, printOption);
pd.PrintPage += PrintImage;
pd.Print();
void PrintImage(object sender, PrintPageEventArgs e)
{
if (streams.Length > 1) e.HasMorePages = true;
foreach (var stream in streams)
{
if (stream.CanSeek) stream.Seek(0, SeekOrigin.Begin);
System.Drawing.Image image = System.Drawing.Image.FromStream(stream);
using Graphics graphics = e.Graphics!;
// 高质量图片
if (printOption.Mode != null)
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
if (printOption.Dpi != null)
{
pd.DefaultPageSettings.PrinterResolution.X = printOption.Dpi.GetValueOrDefault();
pd.DefaultPageSettings.PrinterResolution.Y = printOption.Dpi.GetValueOrDefault();
}
if (printOption.IsAutoScale)
{
var size = GetSize(e.PageBounds, image);
graphics.DrawImage(image, e.MarginBounds.X, e.MarginBounds.Y, size.Width, size.Height);
}
else
{
// 不支持过大的图片跨页
graphics.DrawImage(image, e.MarginBounds.X, e.MarginBounds.Y);
}
}
}
}
private static Size GetSize(Rectangle page, Image image)
{
double imageWidth = image.Width;
double imageHeight = image.Height;
// ClientSize 获取到的才是真正可以显示的区域,去掉了边框,Size 是纸张全部区域
double pageWidth = page.Width;
double pageHeight = page.Height;
// 最终计算结果
double width = image.Width;
double height = image.Height;
// 图片过长时
if (imageWidth >= pageWidth)
{
double ratio = imageWidth / pageWidth;
width = pageWidth;
height = imageHeight / ratio;
}
// 图片小于页面,则自动放大
else if (imageWidth < pageWidth)
{
double ratio = pageWidth / imageWidth;
width = pageWidth;
height = imageHeight * ratio;
}
return new Size(width: (int)width, height: (int)height);
}
打印 pdf:
public static void PrintPdf(Stream stream, PrintOption printOption)
{
if (printOption == null) printOption = new PrintOption();
PdfDocument doc = PdfDocument.Load(stream);
var printDocument = doc.CreatePrintDocument();
BuildOption(printDocument, printOption);
printDocument.Print();
}
由于生成的文件带有一些动态库,导致体积太大,不需要的情况下可以使用脚本自动删除。
<Target Name="DeletePdfiumFile" AfterTargets="Publish" Condition="'$(PublishDir)' != ''">
<Exec WorkingDirectory="./" Command="echo "DEL $(PublishDir)libpdfium.dylib"" />
<Exec WorkingDirectory="./" Command="DEL "$(PublishDir)libpdfium.dylib"" ContinueOnError="true" />
</Target>
写到主项目的 .csproj 文件中。
如果只需要 x64 不需要 x86,那么还可以减小体积。
<Target Name="DeletePdfiumFile" AfterTargets="Publish" Condition="'$(PublishDir)' != ''">
<Exec WorkingDirectory="./" Command="echo "删除pdfium文件"" />
<Exec WorkingDirectory="./" Command="echo "DEL $(PublishDir)x86\pdfium.dll"" />
<Exec WorkingDirectory="./" Command="DEL "$(PublishDir)x86\pdfium.dll"" ContinueOnError="true" />
<Exec WorkingDirectory="./" Command="echo "DEL $(PublishDir)libpdfium.dylib"" />
<Exec WorkingDirectory="./" Command="DEL "$(PublishDir)libpdfium.dylib"" ContinueOnError="true" />
</Target>
文章评论
papersize是像素还是以百分之一英寸为单位?
@徐徐 像素,px
网友需要:
支持双面打印。
支持挂后台,通过 http 调用,可考虑使用 HtpListener 做一个 AOP 服务。