如何使用 C# 将 PDF 打印到 ZPL(Zebra 打印机)?

How to print PDF to ZPL (Zebra Printers) using c#?

ZEBRA PRINTERS 使用称为 ZPL 编程语言的本机命令,将 PDF 打印到打印机通常不起作用,使用 C# 打印的最佳解决方案是?

我在网上找不到有效的解决方案,因此创建了这个问题和答案,这个问题会帮助很多人解决这个问题

以下最佳解决方案:

要求:在电脑上安装免费库Ghostscript 32位 https://www.ghostscript.com/download.html

主要方法:pdfbase64转ZPL或Stream pdf转ZPL

    public static List<string> ZplFromPdf(string pdfBase64, int dpi = 300)
    {
        return ZplFromPdf(new MemoryStream(Convert.FromBase64String(pdfBase64)), new Size(0,0), dpi);
    }

    public static List<string> ZplFromPdf(Stream pdf, Size size, int dpi = 300)
    {
        var zpls = new List<string>();

        if (size == new Size(0, 0))
        {
            size = new Size(812, 1218);
        }

        using (var rasterizer = new GhostscriptRasterizer())
        {
            rasterizer.Open(pdf);

            var images = new List<Image>();
            for (int pageNumber = 1; pageNumber <= rasterizer.PageCount; pageNumber++)
            {
                var bmp = new Bitmap(rasterizer.GetPage(dpi, dpi, pageNumber), size.Width, size.Height);

                var zpl = new StringBuilder();
                zpl.Append(ZPLHelper.GetGrfStoreCommand("R:LBLRA2.GRF", bmp));
                zpl.Append("^XA^FO0,0^XGR:LBLRA2.GRF,1,1^FS^XZ");
                zpl.Append("^XA^IDR:LBLRA2.GRF^FS^XZ");

                zpls.Add(zpl.ToString());
            }
            return zpls;
        }
    }

核心方法

public class ZPLHelper
{
static Regex regexFilename = new Regex("^[REBA]:[A-Z0-9]{1,8}\.GRF$");

public static bool PrintLabelBase64Image(string printerName, string base64Image, string jobName = "label")
{
try
{
var bmpLabel = Base64ToBitmap(base64Image);
var baseStream = new MemoryStream();
var tw = new StreamWriter(baseStream, Encoding.UTF8);
tw.WriteLine(GetGrfStoreCommand("R:LBLRA2.GRF", bmpLabel));
tw.WriteLine(GetGrfPrintCommand("R:LBLRA2.GRF"));
tw.WriteLine(GetGrfDeleteCommand("R:LBLRA2.GRF"));
tw.Flush();
baseStream.Position = 0;

var gdipj = new GdiPrintJob(printerName, GdiPrintJobDataType.Raw, jobName, null);
gdipj.WritePage(baseStream);
gdipj.CompleteJob();

return true;
}
catch (Exception)
{
return false;
}
}
public static bool PrintLabelZpl(string printerName, string zplCommand, string jobName = "label")
{
var baseStream = new MemoryStream();
var tw = new StreamWriter(baseStream, Encoding.UTF8);
tw.WriteLine(zplCommand);
tw.Flush();
baseStream.Position = 0;

var gdiJob = new GdiPrintJob(printerName, GdiPrintJobDataType.Raw, jobName, null);
gdiJob.WritePage(baseStream);
gdiJob.CompleteJob();

return true;
}

private static Bitmap Base64ToBitmap(string base64Image)
{
Image image;
using (var ms = new MemoryStream(Convert.FromBase64String(base64Image)))
{
image = Image.FromStream(ms);
}

return new Bitmap(image);
}
public static string GetGrfStoreCommand(string filename, Bitmap bmpSource)
{
if (bmpSource == null)
{
throw new ArgumentNullException("bmpSource");
}
validateFilename(filename);

var dim = new Rectangle(Point.Empty, bmpSource.Size);
var stride = ((dim.Width + 7) / 8);
var bytes = stride * dim.Height;

using (var bmpCompressed = bmpSource.Clone(dim, PixelFormat.Format1bppIndexed))
{
var result = new StringBuilder();

result.AppendFormat("^XA~DG{2},{0},{1},", stride * dim.Height, stride, filename);
byte[][] imageData = GetImageData(dim, stride, bmpCompressed);

byte[] previousRow = null;
foreach (var row in imageData)
{
appendLine(row, previousRow, result);
previousRow = row;
}
result.Append(@"^FS^XZ");

return result.ToString();
}
}
public static string GetGrfDeleteCommand(string filename)
{
validateFilename(filename);

return string.Format("^XA^ID{0}^FS^XZ", filename);
}
public static string GetGrfPrintCommand(string filename)
{
validateFilename(filename);

return string.Format("^XA^FO0,0^XG{0},1,1^FS^XZ", filename);
}

private static void validateFilename(string filename)
{
if (!regexFilename.IsMatch(filename))
{
throw new ArgumentException("Filename must be in the format "
+ "R:XXXXXXXX.GRF.  Drives are R, E, B, A.  Filename can "
+ "be alphanumeric between 1 and 8 characters.", "filename");
}
}
unsafe private static byte[][] GetImageData(Rectangle dim, int stride, Bitmap bmpCompressed)
{
byte[][] imageData;
var data = bmpCompressed.LockBits(dim, ImageLockMode.ReadOnly, PixelFormat.Format1bppIndexed);
try
{
byte* pixelData = (byte*)data.Scan0.ToPointer();
byte rightMask = (byte)(0xff << (data.Stride * 8 - dim.Width));
imageData = new byte[dim.Height][];

for (int row = 0; row < dim.Height; row++)
{
byte* rowStart = pixelData + row * data.Stride;
imageData[row] = new byte[stride];

for (int col = 0; col < stride; col++)
{
byte f = (byte)(0xff ^ rowStart[col]);
f = (col == stride - 1) ? (byte)(f & rightMask) : f;
imageData[row][col] = f;
}
}
}
finally
{
bmpCompressed.UnlockBits(data);
}
return imageData;
}
private static void appendLine(byte[] row, byte[] previousRow, StringBuilder baseStream)
{
if (row.All(r => r == 0))
{
baseStream.Append(",");
return;
}

if (row.All(r => r == 0xff))
{
baseStream.Append("!");
return;
}

if (previousRow != null && MatchByteArray(row, previousRow))
{
baseStream.Append(":");
return;
}

byte[] nibbles = new byte[row.Length * 2];
for (int i = 0; i < row.Length; i++)
{
nibbles[i * 2] = (byte)(row[i] >> 4);
nibbles[i * 2 + 1] = (byte)(row[i] & 0x0f);
}

for (int i = 0; i < nibbles.Length; i++)
{
byte cPixel = nibbles[i];

int repeatCount = 0;
for (int j = i; j < nibbles.Length && repeatCount <= 400; j++)
{
if (cPixel == nibbles[j])
{
repeatCount++;
}
else
{
break;
}
}

if (repeatCount > 2)
{
if (repeatCount == nibbles.Length - i
&& (cPixel == 0 || cPixel == 0xf))
{
if (cPixel == 0)
{
    if (i % 2 == 1)
    {
        baseStream.Append("0");
    }
    baseStream.Append(",");
    return;
}
else if (cPixel == 0xf)
{
    if (i % 2 == 1)
    {
        baseStream.Append("F");
    }
    baseStream.Append("!");
    return;
}
}
else
{
baseStream.Append(getRepeatCode(repeatCount));
i += repeatCount - 1;
}
}
baseStream.Append(cPixel.ToString("X"));
}
}
private static string getRepeatCode(int repeatCount)
{
if (repeatCount > 419)
throw new ArgumentOutOfRangeException();

int high = repeatCount / 20;
int low = repeatCount % 20;

const string lowString = " GHIJKLMNOPQRSTUVWXY";
const string highString = " ghijklmnopqrstuvwxyz";

string repeatStr = "";
if (high > 0)
{
repeatStr += highString[high];
}
if (low > 0)
{
repeatStr += lowString[low];
}

return repeatStr;
}
private static bool MatchByteArray(byte[] row, byte[] previousRow)
{
for (int i = 0; i < row.Length; i++)
{
if (row[i] != previousRow[i])
{
return false;
}
}

return true;
}
}

internal static class NativeMethods
{
#region winspool.drv

#region P/Invokes

[DllImport("winspool.Drv", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool OpenPrinter(string szPrinter, out IntPtr hPrinter, IntPtr pd);

[DllImport("winspool.Drv", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool ClosePrinter(IntPtr hPrinter);

[DllImport("winspool.Drv", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern UInt32 StartDocPrinter(IntPtr hPrinter, Int32 level, IntPtr di);

[DllImport("winspool.Drv", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool EndDocPrinter(IntPtr hPrinter);

[DllImport("winspool.Drv", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool StartPagePrinter(IntPtr hPrinter);

[DllImport("winspool.Drv", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool EndPagePrinter(IntPtr hPrinter);

[DllImport("winspool.Drv", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern bool WritePrinter(
// 0
IntPtr hPrinter,
[MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2)] byte[] pBytes,
// 2
UInt32 dwCount,
out UInt32 dwWritten);

#endregion

#region Structs

[StructLayout(LayoutKind.Sequential)]
internal struct DOC_INFO_1
{
[MarshalAs(UnmanagedType.LPWStr)]
public string DocName;
[MarshalAs(UnmanagedType.LPWStr)]
public string OutputFile;
[MarshalAs(UnmanagedType.LPWStr)]
public string Datatype;
}

#endregion

#endregion
}

/// <summary>
/// Represents a print job in a spooler queue
/// </summary>
public class GdiPrintJob
{
IntPtr PrinterHandle;
//IntPtr DocHandle;

/// <summary>
/// The ID assigned by the print spooler to identify the job
/// </summary>
public UInt32 PrintJobID { get; private set; }

/// <summary>
/// Create a print job with a enumerated datatype
/// </summary>
/// <param name="PrinterName"></param>
/// <param name="dataType"></param>
/// <param name="jobName"></param>
/// <param name="outputFileName"></param>
public GdiPrintJob(string PrinterName, GdiPrintJobDataType dataType, string jobName, string outputFileName)
: this(PrinterName, translateType(dataType), jobName, outputFileName)
{
}

/// <summary>
/// Create a print job with a string datatype
/// </summary>
/// <param name="PrinterName"></param>
/// <param name="dataType"></param>
/// <param name="jobName"></param>
/// <param name="outputFileName"></param>
public GdiPrintJob(string PrinterName, string dataType, string jobName, string outputFileName)
{
if (string.IsNullOrWhiteSpace(PrinterName))
throw new ArgumentNullException("PrinterName");
if (string.IsNullOrWhiteSpace(dataType))
throw new ArgumentNullException("PrinterName");

IntPtr hPrinter;
if (!NativeMethods.OpenPrinter(PrinterName, out hPrinter, IntPtr.Zero))
throw new Win32Exception();
this.PrinterHandle = hPrinter;

NativeMethods.DOC_INFO_1 docInfo = new NativeMethods.DOC_INFO_1()
{
DocName = jobName,
Datatype = dataType,
OutputFile = outputFileName
};
IntPtr pDocInfo = Marshal.AllocHGlobal(Marshal.SizeOf(docInfo));
RuntimeHelpers.PrepareConstrainedRegions();
try
{
Marshal.StructureToPtr(docInfo, pDocInfo, false);
UInt32 docid = NativeMethods.StartDocPrinter(hPrinter, 1, pDocInfo);
if (docid == 0)
throw new Win32Exception();
this.PrintJobID = docid;
}
finally
{
Marshal.FreeHGlobal(pDocInfo);
}
}

/// <summary>
/// Write the data of a single page or a precomposed PCL document
/// </summary>
/// <param name="data"></param>
public void WritePage(Stream data)
{
if (data == null)
throw new ArgumentNullException("data");
if (!data.CanRead && !data.CanWrite)
throw new ObjectDisposedException("data");
if (!data.CanRead)
throw new NotSupportedException("stream is not readable");

if (!NativeMethods.StartPagePrinter(this.PrinterHandle))
throw new Win32Exception();

byte[] buffer = new byte[0x14000]; /* 80k is Stream.CopyTo default */
uint read = 1;
while ((read = (uint)data.Read(buffer, 0, buffer.Length)) != 0)
{
UInt32 written;
if (!NativeMethods.WritePrinter(this.PrinterHandle, buffer, read, out written))
throw new Win32Exception();

if (written != read)
throw new InvalidOperationException("Error while writing to stream");
}

if (!NativeMethods.EndPagePrinter(this.PrinterHandle))
throw new Win32Exception();
}

/// <summary>
/// Complete the current job
/// </summary>
public void CompleteJob()
{
if (!NativeMethods.EndDocPrinter(this.PrinterHandle))
throw new Win32Exception();
}

#region datatypes
private readonly static string[] dataTypes = new string[]
{ 
// 0
null,
"RAW", 
// 2
"RAW [FF appended]",
"RAW [FF auto]",
// 4
"NT EMF 1.003",
"NT EMF 1.006",
// 6
"NT EMF 1.007",
"NT EMF 1.008", 
// 8
"TEXT",
"XPS_PASS", 
// 10
"XPS2GDI"
};

private static string translateType(GdiPrintJobDataType type)
{
return dataTypes[(int)type];
}
#endregion
}

public enum GdiPrintJobDataType
{
Unknown = 0,
Raw = 1,
RawAppendFF = 2,
RawAuto = 3,
NtEmf1003 = 4,
NtEmf1006 = 5,
NtEmf1007 = 6,
NtEmf1008 = 7,
Text = 8,
XpsPass = 9,
Xps2Gdi = 10
}

您可能对我的 NuGet package for converting PDF files into ZPL code 感兴趣。 official Zebra SDK 可能包含转换,但我没有检查(不确定此处的许可)。

为第一页调整您之前的代码将是

public static string ZplFromPdf(string pdfBase64, int dpi = 300)
{
    return PDFtoZPL.Conversion.ConvertPdfPage(pdfBase64, dpi: dpi);
}

免责声明:我 运行 遇到了同样的问题,将在互联网上找到的 ZPL 代码拼接在一起并将其捆绑到一个微型 .NET API.