// cupsprinterinterface.js - CUPS printer interface implementation import log4js from "log4js"; import { loadConfig } from "../../config.js"; import ipp from "ipp"; const config = loadConfig(); const logger = log4js.getLogger("CUPS Printer Interface"); logger.level = config.logLevel; export default class CupsInterface { constructor(documentPrinterClient) { this.documentPrinterClient = documentPrinterClient; this.host = documentPrinterClient.connection.host; this.port = documentPrinterClient.connection.port || 631; this.interface = documentPrinterClient.connection.interface || "cups"; this.protocol = documentPrinterClient.connection.protocol; this.name = documentPrinterClient.documentPrinter.name; this.isConnected = false; this.cupsPrinter = null; this.pdfs = new Map(); } /** * Build the IPP printer URL from connection settings */ buildPrinterUrl() { var hostWithPort = `${this.host}:${this.port}`; var path = "/"; if (this.host.includes("/")) { const parts = this.host.split("/"); var hostName = parts[0]; if (hostName.includes(":")) { const portHost = hostName.split(":"); hostName = portHost[0]; } hostWithPort = `${hostName}:${this.port}`; path += parts.slice(1).join("/"); } switch (this.protocol) { case "ipp": return `ipp://${hostWithPort}${path}`; case "http": return `http://${hostWithPort}${path}`; default: logger.warn(`Unknown protocol ${this.protocol}, defaulting to ipp.`); return `ipp://${hostWithPort}${path}`; } } /** * Promisify IPP execute method */ async executeIPP(operation, message) { return new Promise((resolve, reject) => { this.cupsPrinter.execute(operation, message, (err, res) => { if (err) { reject(err); } else { resolve(res); } }); }); } async connect() { logger.info(`Connecting to CUPS printer ${this.name}`); try { this.cupsPrinterUrl = this.buildPrinterUrl(); logger.debug(`Printer URL: ${this.cupsPrinterUrl}`); // Create IPP printer instance this.cupsPrinter = ipp.Printer(this.cupsPrinterUrl); // Test connection by getting printer attributes const getPrinterAttributes = { "operation-attributes-tag": { "requested-attributes": [ "printer-name", "printer-state", "printer-state-message", "printer-uri-supported", ], }, }; try { const response = await this.executeIPP( "Get-Printer-Attributes", getPrinterAttributes ); const printerState = response["printer-attributes-tag"]?.["printer-state"]; logger.info( `Successfully connected to CUPS printer ${this.name}. State: ${printerState}` ); this.isConnected = true; return true; } catch (error) { logger.error( `Failed to get printer attributes for ${this.name}:`, error ); return { error: "Failed to get printer attributes. " + error.message, }; } } catch (error) { logger.error(`Failed to connect to CUPS printer:`, error); this.isConnected = false; this.cupsPrinter = null; return { error: "Failed to connect to CUPS printer. " + error.message, }; } } async disconnect() { logger.info(`Disconnecting from CUPS printer...`); this.isConnected = false; this.cupsPrinter = null; return { success: true }; } async initialize() { logger.info(`Initializing CUPS printer...`); if (!this.isConnected || !this.cupsPrinter) { return { error: "Printer is not connected" }; } try { // Verify printer is ready by checking attributes const getPrinterAttributes = { "operation-attributes-tag": { "requested-attributes": [ "printer-state", "printer-state-message", "printer-state-reasons", ], }, }; const response = await this.executeIPP( "Get-Printer-Attributes", getPrinterAttributes ); const printerState = response["printer-attributes-tag"]?.["printer-state"]; const stateMessage = response["printer-attributes-tag"]?.["printer-state-message"] || ""; // Printer states: 3 = idle, 4 = processing, 5 = stopped if (printerState === 5) { logger.warn(`Printer ${this.name} is stopped: ${stateMessage}`); return { error: `Printer is stopped: ${stateMessage}` }; } logger.info( `CUPS printer ${this.name} initialized successfully. State: ${printerState}` ); return true; } catch (error) { logger.error(`Failed to initialize CUPS printer ${this.name}:`, error); return { error: error.message || "Failed to initialize CUPS printer" }; } } /** * Get document data from documentJob * Supports file references, buffers, and content */ async getDocumentData(documentJob) { let data = null; // If documentJob has a file reference, fetch it if (documentJob.file || documentJob.fileId) { const fileId = documentJob.file?._id || documentJob.file || documentJob.fileId; if (this.documentPrinterClient.socketClient?.fileManager) { data = await this.documentPrinterClient.socketClient.fileManager.getFile( fileId ); } else { throw new Error("File manager not available to fetch file"); } } // If documentJob has a buffer directly else if (documentJob.buffer || documentJob.data) { data = documentJob.buffer || documentJob.data; } // If documentJob has rendered image else if (documentJob.renderedImage) { data = documentJob.renderedImage; } // If documentJob has content (text), convert to buffer else if (documentJob.content) { data = Buffer.from(documentJob.content, "utf-8"); } else { throw new Error("Document job has no printable content"); } // Ensure data is a Buffer if (!Buffer.isBuffer(data)) { if (data instanceof Uint8Array) { data = Buffer.from(data); } else if (data instanceof ArrayBuffer) { data = Buffer.from(data); } else if (typeof data === "string") { data = Buffer.from(data, "utf-8"); } else { throw new Error( "Document data must be a Buffer, Uint8Array, ArrayBuffer, or string" ); } } return data; } async deploy(documentJob, pdf) { logger.info( `Deploying job ${documentJob._id} to CUPS printer ${this.name}` ); // Store the PDF buffer this.pdfs.set(documentJob._id, pdf); return { success: true }; } async print(jobId) { logger.info(`Printing job ${jobId} to CUPS printer ${this.name}`); if (!this.isConnected || !this.cupsPrinter) { throw new Error("Printer is not connected"); } const pdf = this.pdfs.get(jobId); if (!pdf) { throw new Error("PDF not found for job"); } try { // Ensure PDF is a Buffer let documentData = pdf; if (!Buffer.isBuffer(documentData)) { if (documentData instanceof Uint8Array) { documentData = Buffer.from(documentData); } else if (documentData instanceof ArrayBuffer) { documentData = Buffer.from(documentData); } else if (typeof documentData === "string") { documentData = Buffer.from(documentData, "utf-8"); } else { throw new Error( "PDF data must be a Buffer, Uint8Array, ArrayBuffer, or string" ); } } // Build IPP print job message const jobName = jobId?.toString() || "Print Job"; const userName = process.env.USER || "system"; const printJobMessage = { "operation-attributes-tag": { "requesting-user-name": userName, "job-name": jobName, "document-format": "application/pdf", }, data: documentData, }; // Send print job logger.debug(`Sending print job ${jobId} to ${this.cupsPrinterUrl}`); const response = await this.executeIPP("Print-Job", printJobMessage); // Extract job ID from response const ippJobId = response["job-attributes-tag"]?.["job-id"]; const jobUri = response["job-attributes-tag"]?.["job-uri"]; logger.info( `Successfully printed job ${jobId} to CUPS printer ${this.name}. IPP Job ID: ${ippJobId}` ); return { success: true, jobId: ippJobId, jobUri: jobUri, }; } catch (error) { logger.error( `Failed to print job ${jobId} to CUPS printer ${this.name}:`, error ); return { success: false, error: error.message || "Failed to print job", }; } } }