/** * Print Feature * Provides document printing capability using IPP protocol */ import { Feature, type TDeviceReference } from './feature.abstract.js'; import { IppProtocol } from '../protocols/index.js'; import type { TPrintProtocol, TPrintSides, TPrintQuality, TPrintColorMode, IPrintCapabilities, IPrintOptions, IPrintJob, IPrintFeatureInfo, IFeatureOptions, } from '../interfaces/feature.interfaces.js'; import type { IPrinterCapabilities } from '../interfaces/index.js'; /** * Options for creating a PrintFeature */ export interface IPrintFeatureOptions extends IFeatureOptions { protocol?: TPrintProtocol; uri?: string; supportsColor?: boolean; supportsDuplex?: boolean; supportedMediaSizes?: string[]; supportedMediaTypes?: string[]; maxCopies?: number; } /** * Print Feature - provides document printing capability * * Wraps the IPP protocol to provide a unified printing interface. * * @example * ```typescript * const printFeature = device.getFeature('print'); * if (printFeature) { * await printFeature.connect(); * const job = await printFeature.print(pdfBuffer, { copies: 2 }); * console.log(`Print job ${job.id} created`); * } * ``` */ export class PrintFeature extends Feature { public readonly type = 'print' as const; public readonly protocol: TPrintProtocol; // Protocol client private ippClient: IppProtocol | null = null; // Configuration public readonly uri: string; // Capabilities public supportsColor: boolean = true; public supportsDuplex: boolean = false; public supportedMediaSizes: string[] = ['iso_a4_210x297mm', 'na_letter_8.5x11in']; public supportedMediaTypes: string[] = ['stationery']; public maxCopies: number = 99; public supportedSides: TPrintSides[] = ['one-sided']; public supportedQualities: TPrintQuality[] = ['normal']; constructor( device: TDeviceReference, port: number, options?: IPrintFeatureOptions ) { super(device, port, options); this.protocol = options?.protocol ?? 'ipp'; this.uri = options?.uri ?? `ipp://${device.address}:${port}/ipp/print`; // Set capabilities from options if provided if (options?.supportsColor !== undefined) this.supportsColor = options.supportsColor; if (options?.supportsDuplex !== undefined) { this.supportsDuplex = options.supportsDuplex; if (options.supportsDuplex) { this.supportedSides = ['one-sided', 'two-sided-long-edge', 'two-sided-short-edge']; } } if (options?.supportedMediaSizes) this.supportedMediaSizes = options.supportedMediaSizes; if (options?.supportedMediaTypes) this.supportedMediaTypes = options.supportedMediaTypes; if (options?.maxCopies) this.maxCopies = options.maxCopies; } // ============================================================================ // Connection // ============================================================================ protected async doConnect(): Promise { if (this.protocol === 'ipp') { // Parse URI to get address, port, and path const url = new URL(this.uri.replace('ipp://', 'http://').replace('ipps://', 'https://')); const address = url.hostname; const port = parseInt(url.port) || this._port; const path = url.pathname || '/ipp/print'; this.ippClient = new IppProtocol(address, port, path); // Verify connection by getting printer attributes const attrs = await this.ippClient.getAttributes(); this.updateCapabilitiesFromIpp(attrs); } // JetDirect and LPD don't need connection verification } protected async doDisconnect(): Promise { this.ippClient = null; } // ============================================================================ // Printing Operations // ============================================================================ /** * Get printer capabilities */ public async getCapabilities(): Promise { return { colorSupported: this.supportsColor, duplexSupported: this.supportsDuplex, mediaSizes: this.supportedMediaSizes, mediaTypes: this.supportedMediaTypes, resolutions: [300, 600], maxCopies: this.maxCopies, sidesSupported: this.supportedSides, qualitySupported: this.supportedQualities, }; } /** * Print a document */ public async print(data: Buffer, options?: IPrintOptions): Promise { if (!this.isConnected) { throw new Error('Print feature not connected'); } if (this.protocol === 'ipp' && this.ippClient) { return this.printWithIpp(data, options); } throw new Error(`Protocol ${this.protocol} not supported yet`); } /** * Get active print jobs */ public async getJobs(): Promise { if (!this.isConnected || !this.ippClient) { throw new Error('Print feature not connected'); } return this.ippClient.getJobs(); } /** * Get info about a specific job */ public async getJobInfo(jobId: number): Promise { if (!this.isConnected || !this.ippClient) { throw new Error('Print feature not connected'); } return this.ippClient.getJobInfo(jobId); } /** * Cancel a print job */ public async cancelJob(jobId: number): Promise { if (!this.isConnected || !this.ippClient) { throw new Error('Print feature not connected'); } await this.ippClient.cancelJob(jobId); this.emit('job:cancelled', jobId); } // ============================================================================ // Protocol-Specific Printing // ============================================================================ private async printWithIpp(data: Buffer, options?: IPrintOptions): Promise { if (!this.ippClient) { throw new Error('IPP client not initialized'); } this.emit('print:started', options); // IppProtocol.print() accepts IPrintOptions and returns IPrintJob const job = await this.ippClient.print(data, options); this.emit('print:submitted', job); return job; } // ============================================================================ // Helper Methods // ============================================================================ private updateCapabilitiesFromIpp(caps: IPrinterCapabilities): void { this.supportsColor = caps.colorSupported; this.supportsDuplex = caps.duplexSupported; this.maxCopies = caps.maxCopies; if (caps.mediaSizes && caps.mediaSizes.length > 0) { this.supportedMediaSizes = caps.mediaSizes; } if (caps.mediaTypes && caps.mediaTypes.length > 0) { this.supportedMediaTypes = caps.mediaTypes; } if (caps.sidesSupported && caps.sidesSupported.length > 0) { this.supportedSides = caps.sidesSupported.filter((s): s is TPrintSides => ['one-sided', 'two-sided-long-edge', 'two-sided-short-edge'].includes(s) ); } if (caps.qualitySupported && caps.qualitySupported.length > 0) { this.supportedQualities = caps.qualitySupported.filter((q): q is TPrintQuality => ['draft', 'normal', 'high'].includes(q) ); } } private qualityToIpp(quality: TPrintQuality): number { switch (quality) { case 'draft': return 3; case 'normal': return 4; case 'high': return 5; default: return 4; } } private mapIppJob(job: Record): IPrintJob { const stateMap: Record = { 3: 'pending', 4: 'pending', 5: 'processing', 6: 'processing', 7: 'canceled', 8: 'aborted', 9: 'completed', }; return { id: job['job-id'] as number, name: job['job-name'] as string ?? 'Unknown', state: stateMap[(job['job-state'] as number) ?? 3] ?? 'pending', stateReason: (job['job-state-reasons'] as string[])?.[0], createdAt: new Date((job['time-at-creation'] as number) * 1000), completedAt: job['time-at-completed'] ? new Date((job['time-at-completed'] as number) * 1000) : undefined, }; } // ============================================================================ // Serialization // ============================================================================ public getFeatureInfo(): IPrintFeatureInfo { return { ...this.getBaseFeatureInfo(), type: 'print', protocol: this.protocol, supportsColor: this.supportsColor, supportsDuplex: this.supportsDuplex, supportedMediaSizes: this.supportedMediaSizes, }; } // ============================================================================ // Static Factory // ============================================================================ /** * Create from discovery metadata */ public static fromDiscovery( device: TDeviceReference, port: number, protocol: TPrintProtocol, metadata: Record ): PrintFeature { const txtRecords = metadata.txtRecords as Record ?? {}; return new PrintFeature(device, port, { protocol, uri: metadata.uri as string, supportsColor: txtRecords['Color'] === 'T' || txtRecords['color'] === 'true', supportsDuplex: txtRecords['Duplex'] === 'T' || txtRecords['duplex'] === 'true', }); } }