import { Device } from '../abstract/device.abstract.js'; import { IppProtocol } from './printer.classes.ippprotocol.js'; import type { IPrinterInfo, IPrinterCapabilities, IPrintOptions, IPrintJob, IRetryOptions, } from '../interfaces/index.js'; /** * Printer class for IPP network printers */ export class Printer extends Device { public readonly uri: string; public supportsColor: boolean = false; public supportsDuplex: boolean = false; public supportedMediaTypes: string[] = []; public supportedMediaSizes: string[] = []; public maxCopies: number = 99; private ippClient: IppProtocol | null = null; private ippPath: string; constructor( info: IPrinterInfo, options?: { ippPath?: string; retryOptions?: IRetryOptions; } ) { super(info, options?.retryOptions); this.uri = info.uri; this.supportsColor = info.supportsColor; this.supportsDuplex = info.supportsDuplex; this.supportedMediaTypes = info.supportedMediaTypes; this.supportedMediaSizes = info.supportedMediaSizes; this.maxCopies = info.maxCopies; this.ippPath = options?.ippPath ?? '/ipp/print'; } /** * Create a Printer from discovery info */ public static fromDiscovery( discoveredDevice: { id: string; name: string; address: string; port: number; txtRecords: Record; }, retryOptions?: IRetryOptions ): Printer { // Parse capabilities from TXT records const txtRecords = discoveredDevice.txtRecords; // Get IPP path from TXT records const rp = txtRecords['rp'] || 'ipp/print'; const ippPath = rp.startsWith('/') ? rp : `/${rp}`; // Parse color support const colorSupported = txtRecords['Color'] === 'T' || txtRecords['color'] === 'true' || txtRecords['URF']?.includes('W8') || false; // Parse duplex support const duplexSupported = txtRecords['Duplex'] === 'T' || txtRecords['duplex'] === 'true' || txtRecords['URF']?.includes('DM') || false; // Build printer URI const isSecure = txtRecords['TLS'] === '1' || discoveredDevice.port === 443; const protocol = isSecure ? 'ipps' : 'ipp'; const uri = `${protocol}://${discoveredDevice.address}:${discoveredDevice.port}${ippPath}`; const info: IPrinterInfo = { id: discoveredDevice.id, name: discoveredDevice.name, type: 'printer', address: discoveredDevice.address, port: discoveredDevice.port, status: 'online', uri: uri, supportsColor: colorSupported, supportsDuplex: duplexSupported, supportedMediaTypes: [], supportedMediaSizes: [], maxCopies: 99, manufacturer: txtRecords['usb_MFG'] || txtRecords['mfg'], model: txtRecords['usb_MDL'] || txtRecords['mdl'] || txtRecords['ty'], }; return new Printer(info, { ippPath, retryOptions }); } /** * Get printer info */ public getPrinterInfo(): IPrinterInfo { return { ...this.getInfo(), type: 'printer', uri: this.uri, supportsColor: this.supportsColor, supportsDuplex: this.supportsDuplex, supportedMediaTypes: this.supportedMediaTypes, supportedMediaSizes: this.supportedMediaSizes, maxCopies: this.maxCopies, }; } /** * Get printer capabilities */ public async getCapabilities(): Promise { if (!this.isConnected) { await this.connect(); } if (!this.ippClient) { throw new Error('IPP client not initialized'); } const caps = await this.withRetry(() => this.ippClient!.getAttributes()); // Update local properties this.supportsColor = caps.colorSupported; this.supportsDuplex = caps.duplexSupported; this.supportedMediaSizes = caps.mediaSizes; this.supportedMediaTypes = caps.mediaTypes; this.maxCopies = caps.maxCopies; return caps; } /** * Print a document */ public async print(data: Buffer, options?: IPrintOptions): Promise { if (!this.isConnected) { await this.connect(); } if (!this.ippClient) { throw new Error('IPP client not initialized'); } this.setStatus('busy'); this.emit('print:started', options); try { const job = await this.withRetry(() => this.ippClient!.print(data, options)); this.setStatus('online'); this.emit('print:submitted', job); return job; } catch (error) { this.setStatus('online'); this.emit('print:error', error); throw error; } } /** * Get all print jobs */ public async getJobs(): Promise { if (!this.isConnected) { await this.connect(); } if (!this.ippClient) { throw new Error('IPP client not initialized'); } return this.withRetry(() => this.ippClient!.getJobs()); } /** * Get specific job info */ public async getJobInfo(jobId: number): Promise { if (!this.isConnected) { await this.connect(); } if (!this.ippClient) { throw new Error('IPP client not initialized'); } return this.withRetry(() => this.ippClient!.getJobInfo(jobId)); } /** * Cancel a print job */ public async cancelJob(jobId: number): Promise { if (!this.isConnected) { await this.connect(); } if (!this.ippClient) { throw new Error('IPP client not initialized'); } await this.withRetry(() => this.ippClient!.cancelJob(jobId)); this.emit('print:canceled', jobId); } /** * Connect to the printer */ protected async doConnect(): Promise { this.ippClient = new IppProtocol(this.address, this.port, this.ippPath); // Test connection by checking availability const available = await this.ippClient.checkAvailability(); if (!available) { throw new Error('Printer not available'); } // Fetch capabilities to populate local properties await this.getCapabilities(); } /** * Disconnect from the printer */ protected async doDisconnect(): Promise { this.ippClient = null; } /** * Refresh printer status */ public async refreshStatus(): Promise { try { if (this.ippClient) { const available = await this.ippClient.checkAvailability(); this.setStatus(available ? 'online' : 'offline'); } else { this.setStatus('offline'); } } catch (error) { this.setStatus('error'); throw error; } } } export { IppProtocol };