Печать изображений PNG на сетевой принтер Zebra
Я пытаюсь найти способ печати изображений на зебру и иметь много проблем.
Согласно документам:
Первая кодировка, известная как B64, кодирует данные с использованием MIME Base64. Base64 используется для кодирования электронных писем...
Base64 кодирует шесть бит в байт, для расширения 33% по незакрытым данным. Вторая кодировка, известная как Z64, сначала сжимает данные, используя алгоритм LZ77, чтобы уменьшить его размер. (Этот алгоритм используется PKZIP и является межстрочным в PNG графический формат.)
Сжатые данные затем кодируются с использованием MIME Base64, как описано выше.
Будет рассчитан CRC по данным, закодированным в Base64.
Но у него нет больше информации.
В основном я пытался кодировать с помощью
private byte[] GetItemFromPath(string filepath)
{
using (MemoryStream ms = new MemoryStream())
{
using (Image img = Image.FromFile(filepath))
{
img.Save(ms, ImageFormat.Png);
return ms.ToArray();
}
}
}
Затем попытаемся напечатать что-то вроде:
var initialArray = GetItemFromPath("C:\\RED.png");
string converted = Convert.ToBase64String(b);
PrintThis(string.Format(@"~DYRED.PNG,P,P,{1},0,:B64:
{0}
^XA
^F0200,200^XGRED.PNG,1,1^FS
^XZ", converted .ToString(), initialArray.Length));
Из его звуков принимаются либо B64, либо Z64.
Я пробовал несколько вариантов и несколько методов для генерации CRC и вычисления "размера".
Но ни один из них не работает, и загрузка графики на принтер всегда прерывается.
Кто-нибудь смог сделать что-то подобное? Или знает, где я ошибаюсь?
Ответы
Ответ 1
Весь кредит для меня, приходящий на этот ответ, был от пользователя LabView Forum пользователя Raydur. Он публикует решение LabView, которое можно открыть в LabView для отправки изображений. Я лично не запускал его с помощью своего принтера, я просто использовал его для определения правильного кода изображения, чтобы я мог реплицировать его в своем коде. Большая вещь, которую мне не хватало, - это заполнить мой шестнадцатеричный код. Например: 1A отлично, но если у вас есть только A, вам нужно наложить 0 перед ним, чтобы отправить 0A. Размер файла в ZPL, который вы отправляете, также является исходным размером массива байтов, а не окончательным строковым представлением данных.
Я просмотрел много, много, много форумов и сообщений Stackoverflow, пытаясь понять это, потому что это похоже на такую простую вещь. Я пробовал каждое решение, размещенное в другом месте, но я действительно хотел просто распечатать .PNG, потому что руководство для моего принтера (Mobile QLN320) поддерживает его встроенный. Он говорит, что отправил его в Base64 или Hexadecimal, и я попробовал оба безрезультатно. Для тех, кто хочет сделать Base64, я нашел в более старом руководстве, что вам нужно вручную вычислять коды CRC для каждого отправляемого вами пакета, поэтому я решил пойти с более легким шестнадцатеричным маршрутом. Итак, вот код, который я получил, чтобы работать!
string ipAddress = "192.168.1.30";
int port = 6101;
string zplImageData = string.Empty;
//Make sure no transparency exists. I had some trouble with this. This PNG has a white background
string filePath = @"C:\Users\Path\To\Logo.png";
byte[] binaryData = System.IO.File.ReadAllBytes(filePath);
foreach (Byte b in binaryData)
{
string hexRep = String.Format("{0:X}", b);
if (hexRep.Length == 1)
hexRep = "0" + hexRep;
zplImageData += hexRep;
}
string zplToSend = "^XA" + "^MNN" + "^LL500" + "~DYE:LOGO,P,P," + binaryData.Length + ",," + zplImageData+"^XZ";
string printImage = "^XA^FO115,50^IME:LOGO.PNG^FS^XZ";
try
{
// Open connection
System.Net.Sockets.TcpClient client = new System.Net.Sockets.TcpClient();
client.Connect(ipAddress, port);
// Write ZPL String to connection
System.IO.StreamWriter writer = new System.IO.StreamWriter(client.GetStream(),Encoding.UTF8);
writer.Write(zplToSend);
writer.Flush();
writer.Write(printImage);
writer.Flush();
// Close Connection
writer.Close();
client.Close();
}
catch (Exception ex)
{
// Catch Exception
}
Ответ 2
Руководство по программированию ZPL II документирует команду ~DG
и формат GRF (стр. 124) для загрузки изображений. Том второй добавляет сведения о дополнительном формате сжатия (стр. 52).
Сначала вам нужно преобразовать изображение в двухуровневое изображение 1bpp, а затем преобразовать его в шестнадцатеричную кодировку. Вы можете дополнительно сжать изображение, чтобы уменьшить время передачи. Затем вы можете распечатать изображение с помощью команды ^ID
.
Хотя в команде ~DY
есть встроенная поддержка PNG-изображений, она плохо документирована и, похоже, не работает на некоторых моделях принтеров. Формат ZB64 в основном не документирован, и попытки получить дополнительную информацию от поддержки Zebra оказались бесплодными. Если у вас настроено ваше сердце на ZB64, вы можете использовать Java Zebralink SDK (посмотрите ImagePrintDemo.java
и com.zebra.sdk.printer.internal.GraphicsConversionUtilZpl.sendImageToStream
).
Как только у вас есть данные команды, его можно отправить через TCP/IP, если принтер имеет сервер печати, или его можно отправить, записав в формате RAW
на принтер.
Приведенный ниже код печатает 5 кбайт PNG в сжатом GRF с коэффициентом сжатия 13 кБ (60 кБ без сжатия):
class Program
{
static unsafe void Main(string[] args)
{
var baseStream = new MemoryStream();
var tw = new StreamWriter(baseStream, Encoding.UTF8);
using (var bmpSrc = new Bitmap(Image.FromFile(@"label.png")))
{
tw.WriteLine(ZplImage.GetGrfStoreCommand("R:LBLRA2.GRF", bmpSrc));
}
tw.WriteLine(ZplImage.GetGrfPrintCommand("R:LBLRA2.GRF"));
tw.WriteLine(ZplImage.GetGrfDeleteCommand("R:LBLRA2.GRF"));
tw.Flush();
baseStream.Position = 0;
var gdipj = new GdiPrintJob("ZEBRA S4M-200dpi ZPL", GdiPrintJobDataType.Raw, "Raw print", null);
gdipj.WritePage(baseStream);
gdipj.CompleteJob();
}
}
class ZplImage
{
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);
}
static Regex regexFilename = new Regex("^[REBA]:[A-Z0-9]{1,8}\\.GRF$");
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
}
Ответ 3
После просмотра руководства ZPL вам необходимо вычислить Циклическую проверку избыточности (CRC) для изображения. Ниже приведен код C, который вычисляет CRC (источник):
// Update the CRC for transmitted and received data using
// the CCITT 16bit algorithm (X^16 + X^12 + X^5 + 1).
unsigned char ser_data;
static unsigned int crc;
crc = (unsigned char)(crc >> 8) | (crc << 8);
crc ^= ser_data;
crc ^= (unsigned char)(crc & 0xff) >> 4;
crc ^= (crc << 8) << 4;
crc ^= ((crc & 0xff) << 4) << 1;
Вы также можете обратиться к странице Википедии в CRC, так как она содержит и другие примеры кода.
https://en.wikipedia.org/wiki/Cyclic_redundancy_check
Все остальное, что вы отправляете, выглядит хорошо. Я бы изучил использование одного из SDK Zebra. Я знаю, что Android отправит изображение на принтер и сохранит его для вас.
Ответ 4
По какой-то причине я не могу заставить B64 работать, но, к счастью, я смог Google поработать над тем, чтобы Z64 работал (в течение трех дней поиска души) с использованием обычного старого JavaScript.
Где-то еще в Руководстве по программированию ZPL вы натыкаетесь на команду CISDFCRC16 - пусть будет загадочная, а почему бы и нет - секция, в которой говорится:
"Значение поля вычисляется CRC-16 для содержимого указанного файла с использованием полинома CRC16-CCITT, который является x ^ 16 + x ^ 12 + x ^ 5 + 1. Он рассчитывается с использованием исходного CRC 0x0000".
Japanglish в стороне, теперь вы можете проверить Каталог параметризованных CRC-алгоритмов с 16 бит (http://reveng.sourceforge.net/crc-catalogue/16.htm) и искать алгоритм XMODEM, который, как оказалось,
width=16 poly=0x1021 init=0x0000 refin=false refout=false
xorout=0x0000 check=0x31c3 name="XMODEM"
Ага. Затем я начал искать остальную часть кода, который мне нужен, и наткнулся на следующее:
Итак, я прочитал файл как массив байтов (Uint8Array), проанализировал его как строку, скомпилировал его с помощью LZ77, вернул обратно в массив байтов и закодировал его с помощью base64, после чего вычислил CRC и вставил его все в мою команду ZPL ~ DT для экономии около 40%. Красивая.
К сожалению, я разрабатываю собственное решение, поэтому я не могу публиковать код.
Удачи!
-Что мог сделать один человек другой.
Ответ 5
Хотя этот вопрос имеет тег С#, некоторые другие ответы не являются строго С#, поэтому вот ответ с использованием Node 8. 5+ (javascript) с использованием java и Zebra SDK. Те же шаги очень похожи для любого языка .NET, который также может использовать SDK и выполнить запрос POST.
const { promisify } = require('util');
const java = require('java');
java.asyncOptions = {
asyncSuffix: "",
syncSuffix: "Sync",
promiseSuffix: "Promise", // Generate methods returning promises, using the suffix Promise.
promisify
};
// Include all .jar under C:\Program Files\Zebra Technologies\link_os_sdk\PC\v2.14.5198\lib
// in your lib folder
java.classpath.push(__dirname + "/lib/ZSDK_API.jar");
var ByteArrayOutputStream = java.import('java.io.ByteArrayOutputStream');
var ZebraImageFactory = java.import('com.zebra.sdk.graphics.ZebraImageFactory');
var PrinterUtil = java.import('com.zebra.sdk.printer.PrinterUtil');
const main = async function () {
let path = 'C:\\images\\yourimage.png';
let os = new ByteArrayOutputStream();
let image = await ZebraImageFactory.getImagePromise(path);
PrinterUtil.convertGraphicPromise("E:IMAGE.PNG", image, os);
console.log(os.toStringSync()); // junk:Z64:~:CRC
console.log('done');
};
main();
Затем вы можете распечатать изображение через ZPL, как:
^XA
~DYE:IMAGE,P,P,1,,:B64:<YOURB64>:<YOURCRC>
^FO0,0^IME:IMAGE.PNG
^XZ
Используя что-то вроде
await axios.post('${printer.ip}/pstprnt', zpl);