C# 超强打印机操作代码 - 完全免费打印

2023年10月25日 1736点热度 0人点赞 3条评论
内容纲要

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 动态库,示例:

file

bblanchon.PDFium 还有其它系统下的封装库,会自动带上对应系统的 PDFium 动态库。

file

正常情况,编译项目之后,会自动在目录中带出这些动态库, 不过有些情况下,使用 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 下使用,则只需要复制这个文件到项目下面:

file
file

下面开始讲解代码。

定义两个传递配置的模型类:

    /// <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是像素还是以百分之一英寸为单位?

    2024年1月2日
    • 痴者工良

      @徐徐 像素,px

      2024年1月29日
  • 痴者工良

    网友需要:
    支持双面打印。
    支持挂后台,通过 http 调用,可考虑使用 HtpListener 做一个 AOP 服务。

    2023年10月26日