Files
BonNotiz/BonNotiz/RawPrinter.cs
T
2026-06-30 09:06:23 +02:00

398 lines
16 KiB
C#

using System;
using System.IO;
using System.Runtime.InteropServices;
namespace KohaCompanion.Shared
{
/// <summary>
/// Eine Klasse zum Kapseln eines Druckers. Mit der Methode
/// <see cref="RawPrinter.CreateDocument">CreateDocument(String)</see>
/// kann ein neues Dokument im RAW-Format erstellt werden.
///
/// Pro RawPrinter-Instanz kann immer nur eine RawDocumentStream-Instanz
/// gleichzeitig aktiv sein. Nach der Freigabe einer RawDocumentStream-
/// Instanz kann wieder ein neues Dokument erstellt werden.
/// </summary>
/// <remarks>
/// Diese Klasse ist nicht threadsicher. Wenn verschiedene Threads drucken
/// wollen, muss für jeden Thread ein eigenens RawPrinter-Objekt
/// erstellt werden.
///
/// Quelle: https://www.vbarchiv.net/tipps/tipp_2375-kassenbon-drucker-mit-vbnet-oder-c-per-esc-pos-befehle-ansprechen.html
/// Originalautor: Konstantin Preißer
/// Originallizenz: MIT
/// Angepaßt an C# von Anna Christina Naß
/// </remarks>
public class RawPrinter : IDisposable
{
// API-Deklarationen
[DllImport("winspool.Drv", EntryPoint = "OpenPrinterW", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool OpenPrinter(string szPrinter, ref IntPtr hPrinter, IntPtr pd);
[DllImport("winspool.Drv", EntryPoint = "ClosePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool ClosePrinter(IntPtr hPrinter);
[DllImport("winspool.drv", EntryPoint = "GetPrinterDriver2W", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool GetPrinterDriver2(IntPtr hWnd, IntPtr hPrinter, string? pEnvironment, int Level, IntPtr pDriverInfo, int cbBuf, ref int pcbNeeded);
private const int ERROR_INSUFFICIENT_BUFFER = 122;
private const int PRINTER_DRIVER_XPS = 0x2;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private class DRIVER_INFO_8
{
public uint cVersion;
public string? pName;
public string? pEnvironment;
public string? pDriverPath;
public string? pDataFile;
public string? pConfigFile;
public string? pHelpFile;
public string? pDependentFiles;
public string? pMonitorName;
public string? pDefaultDataType;
public string? pszzPreviousNames;
public System.Runtime.InteropServices.ComTypes.FILETIME ftDriverDate;
public ulong dwlDriverVersion;
public string? pszMfgName;
public string? pszOEMUrl;
public string? pszHardwareID;
public string? pszProvider;
public string? pszPrintProcessor;
public string? pszVendorSetup;
public string? pszzColorProfiles;
public string? pszInfPath;
public uint dwPrinterDriverAttributes;
public string? pszzCoreDriverDependencies;
public System.Runtime.InteropServices.ComTypes.FILETIME ftMinInboxDriverVerDate;
public ulong dwlMinInboxDriverVerVersion;
}
// Instanz-Variablen
private readonly IntPtr hPrinter = IntPtr.Zero;
private readonly bool xpsDriver;
private RawDocumentStream? currentDoc = null;
private bool disposed = false;
private IntPtr HandlePrinter
{
get
{
if (disposed)
throw new ObjectDisposedException("RawPrinter");
return hPrinter;
}
}
/// <summary>
/// Erstellt eine neue Instanz.
/// </summary>
/// <param name="printerName">Der Name des zu verwendenden Druckers</param>
/// <exception cref="System.ComponentModel.Win32Exception">Beim Aufruf einer
/// Win32-API trat ein Fehler auf.</exception>
public RawPrinter(string printerName)
{
CheckApiCall(OpenPrinter(printerName, ref hPrinter, IntPtr.Zero));
xpsDriver = IsXpsDriver();
}
/// <summary>
/// Überprüft den Rückgabewert einer Win32-API-Funktion und wirft bei Bedarf
/// eine Exception.
/// </summary>
/// <param name="retVal"></param>
private static void CheckApiCall(bool retVal)
{
if (!retVal)
throw new System.ComponentModel.Win32Exception(Marshal.GetLastWin32Error());
}
/// <summary>
/// Gibt die nativen Resourcen dieses Objekts frei.
/// </summary>
/// <exception cref="System.ComponentModel.Win32Exception">Beim Aufruf einer
/// Win32-API trat ein Fehler auf.</exception>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (!disposed)
{
ClosePrinter(HandlePrinter);
disposed = true;
}
}
~RawPrinter()
{
Dispose(false);
}
private bool canCreateDocument = false;
/// <summary>
/// Erstellt ein neues RawPrintDocument.
/// </summary>
/// <param name="docName">Der Name des Dokuments</param>
/// <returns>Das neue Dokument</returns>
/// <exception cref="System.ComponentModel.Win32Exception">Beim Aufruf einer
/// Win32-API trat ein Fehler auf.</exception>
/// <exception cref="InvalidOperationException">Es wurde bereits ein Dokument
/// für diesen Drucker erstellt und noch nicht freigegeben.</exception>
public RawDocumentStream CreateDocument(string docName)
{
if (disposed)
throw new ObjectDisposedException("RawPrinter");
if (currentDoc != null)
throw new InvalidOperationException("Pro RawPrinter kann immer nur " + "1 RawDocumentStream-Objekt erzeugt werden.");
canCreateDocument = true;
try
{
currentDoc = new RawDocumentStream(this, docName);
return currentDoc;
}
finally
{
canCreateDocument = false;
}
}
/// <summary>
/// Überprüft, ob es sich bei dem Treiber dieses Druckers um einen
/// XPS-Treiber handelt.
/// </summary>
/// <remarks>
/// Bei Windows-Versionen kleiner als Windows 8 (NT 6.2), also
/// Windows Vista und Windows 7, wird immer
/// false zurückgegeben.
/// </remarks>
/// <returns>true, wenn es sich um einen XPS-Treiber handelt</returns>
private bool IsXpsDriver()
{
int level = 8;
// Nachschauen, wieviel Bytes als Buffer reserviert werden müssen.
// In diesem Fall muss GetPrinterDriver2 mit ERROR_INSUFFICIENT_BUFFER
// failen.
int bytesNeeded = 0;
GetPrinterDriver2(IntPtr.Zero, HandlePrinter, null, level, IntPtr.Zero, 0, ref bytesNeeded);
if (Marshal.GetLastWin32Error() != ERROR_INSUFFICIENT_BUFFER)
CheckApiCall(false);
DRIVER_INFO_8 driverInf;
// Nativen Speicher mit geforderter Größe erstellen
int pBytesLength = Math.Max(bytesNeeded, Marshal.SizeOf(typeof(DRIVER_INFO_8)));
IntPtr pBytes = Marshal.AllocHGlobal(pBytesLength);
try
{
CheckApiCall(GetPrinterDriver2(IntPtr.Zero, HandlePrinter, null, level, pBytes, pBytesLength, ref bytesNeeded));
// Der Anfang des Bytesarrays enthält die Structure; der hintere Teil kann
// die Strings enthalten, auf die die Pointer in der Structure zeigen.
// Hier jetzt die Structure in ein verwaltetes Objekt marshallen.
driverInf = (DRIVER_INFO_8)Marshal.PtrToStructure(pBytes, typeof(DRIVER_INFO_8));
}
finally
{
Marshal.FreeHGlobal(pBytes);
pBytes = IntPtr.Zero;
}// Pointer clearen
// Prüfen, ob das Flag PRINTER_DRIVER_XPS gesetzt ist
return (driverInf.dwPrinterDriverAttributes & PRINTER_DRIVER_XPS) != 0;
}
/// <summary>
/// Ermöglicht das Senden von RAW-Dokumenten (Bytes) an den Drucker.
/// </summary>
/// <remarks>
/// Die Methoden StartPage() und EndPage() dienen zum Unterteilen des
/// Druckauftrags in Seiten (wichtig, wenn der Windows Spooler verwendet
/// wird und man Dokumente senden will, die lange zum Erstellen brauchen,
/// da dieser bis zur Fertigestellung einer Seite wartet, bevor diese
/// tatsächlich zum Drucker gesendet wird).
/// </remarks>
public class RawDocumentStream : Stream
{
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private class DOCINFO
{
public string? pDocName;
public string? pOutputFile;
public string? pDataType;
}
// API-Deklarationen
[DllImport("winspool.Drv", EntryPoint = "StartDocPrinterW", SetLastError = true, CharSet = CharSet.Unicode, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool StartDocPrinter(IntPtr hPrinter, Int32 level, [In()][MarshalAs(UnmanagedType.LPStruct)] DOCINFO di);
[DllImport("winspool.Drv", EntryPoint = "EndDocPrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool EndDocPrinter(IntPtr hPrinter);
[DllImport("winspool.Drv", EntryPoint = "StartPagePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool StartPagePrinter(IntPtr hPrinter);
[DllImport("winspool.Drv", EntryPoint = "EndPagePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool EndPagePrinter(IntPtr hPrinter);
[DllImport("winspool.Drv", EntryPoint = "WritePrinter", SetLastError = true, ExactSpelling = true, CallingConvention = CallingConvention.Winapi)]
private static extern bool WritePrinter(IntPtr hPrinter, IntPtr pBytes, int dwCount, ref int dwWritten);
private readonly RawPrinter printer;
private bool pageStarted = false;
private bool disposed = false;
internal RawDocumentStream(RawPrinter printer, string docName)
{
if (!printer.canCreateDocument)
// Schauen, dass das über den Printer erstellt wird
throw new InvalidOperationException();
this.printer = printer;
DOCINFO di = new DOCINFO();
// Wenn es sich um einen v4-Treiber (XPS-basiert, eingeführt mit
// Windows 8) handelt, muss "XPS_PASS" statt "RAW" verwendet werden.
// Siehe: http://support.microsoft.com/kb/2779300
di.pDataType = printer.xpsDriver ? "XPS_PASS" : "RAW";
di.pDocName = docName;
CheckApiCall(StartDocPrinter(printer.HandlePrinter, 1, di));
}
/// <summary>
/// Startet eine neue Seite (nur wichtig für den Windows Spooler).
/// </summary>
/// <exception cref="System.ComponentModel.Win32Exception">Beim
/// Aufruf einer Win32-API trat ein Fehler auf.</exception>
public void StartPage()
{
if (!pageStarted)
{
CheckApiCall(StartPagePrinter(printer.HandlePrinter));
pageStarted = true;
}
}
/// <summary>
/// Beendet die aktuelle Seite.
/// </summary>
/// <exception cref="System.ComponentModel.Win32Exception">Beim
/// Aufruf einer Win32-API trat ein Fehler auf.</exception>
public void FinishPage()
{
if (pageStarted)
{
CheckApiCall(EndPagePrinter(printer.HandlePrinter));
pageStarted = false;
}
}
protected override void Dispose(bool disposing)
{
if (!disposed)
{
if (disposing)
{
if (pageStarted)
FinishPage();
CheckApiCall(EndDocPrinter(printer.HandlePrinter));
printer.currentDoc = null;
}
disposed = true;
}
base.Dispose(disposing);
}
public override bool CanRead { get { return false; } }
public override bool CanSeek { get { return false; } }
public override bool CanWrite { get { return true; } }
public override void Flush() { }
public override long Length { get { throw new NotSupportedException(); } }
public override long Position
{
get { throw new NotSupportedException(); }
set { throw new NotSupportedException(); }
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
/// <summary>
/// Sendet die angegebenen Bytes an den Drucker.
/// </summary>
/// <param name="buffer"></param>
/// <param name="offset"></param>
/// <param name="count"></param>
/// <exception cref="System.ComponentModel.Win32Exception">Beim Aufruf
/// einer Win32-API trat ein Fehler auf.</exception>
public override void Write(byte[] buffer, int offset, int count)
{
if (disposed)
throw new ObjectDisposedException("RawDocumentStream");
if (offset < 0 || count < 0 || offset + count > buffer.Length)
throw new ArgumentException();
if (!pageStarted)
StartPage();
// Pointer auf Byte-Array holen.
// In C#: fixed (byte* pBytes = buffer) { ... },
// dann Pointerarithmetik verwenden
GCHandle hgc = GCHandle.Alloc(buffer, GCHandleType.Pinned);
try
{
int bytesCompleted = 0;
while (count - bytesCompleted > 0)
{
int tempBytesWritten = 0;
CheckApiCall(WritePrinter(printer.HandlePrinter, Marshal.UnsafeAddrOfPinnedArrayElement(buffer, offset + bytesCompleted), count - bytesCompleted, ref tempBytesWritten));
if (!(tempBytesWritten > 0))
break;
bytesCompleted += tempBytesWritten;
}
}
finally
{
hgc.Free();
}
}
}
}
}